diff --git a/libs/shared/lib/components/buttons/Button.tsx b/libs/shared/lib/components/buttons/Button.tsx index 2b0d0d7b34eff95e25245ff7571933d778bfe4ec..f23c1a7c18ca4269dc23ccb3eca04010c603efa2 100644 --- a/libs/shared/lib/components/buttons/Button.tsx +++ b/libs/shared/lib/components/buttons/Button.tsx @@ -2,12 +2,13 @@ import React, { ReactElement, ReactPropTypes, useMemo } from 'react'; import styles from './buttons.module.scss'; import { Icon, Sizes } from '../icon'; import { forwardRef } from 'react'; +import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip'; type ButtonProps = { as?: 'button' | 'a' | 'div'; variantType?: 'primary' | 'secondary' | 'danger'; variant?: 'solid' | 'outline' | 'ghost'; - size?: '2xs' | 'xs' | 'sm' | 'md' | 'lg'; + size?: '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'; label?: string; rounded?: boolean; disabled?: boolean; @@ -20,6 +21,7 @@ type ButtonProps = { children?: React.ReactNode; className?: string; style?: React.CSSProperties; + tooltip?: string; onMouseUp?: (e: any) => void; onMouseDown?: (e: any) => void; onMouseEnter?: (e: any) => void; @@ -52,6 +54,7 @@ export const Button = React.forwardRef<HTMLButtonElement | HTMLAnchorElement | H ariaLabel, className, children, + tooltip, ...props }, forwardedRef, @@ -94,6 +97,10 @@ export const Button = React.forwardRef<HTMLButtonElement | HTMLAnchorElement | H return styles['btn-md']; case 'lg': return styles['btn-lg']; + case 'xl': + return styles['btn-xl']; + case '2xl': + return styles['btn-2xl']; default: return styles['btn-md']; } @@ -111,6 +118,10 @@ export const Button = React.forwardRef<HTMLButtonElement | HTMLAnchorElement | H return 24; case 'lg': return 28; + case 'xl': + return 32; + case '2xl': + return 36; default: return 24; } @@ -134,20 +145,25 @@ export const Button = React.forwardRef<HTMLButtonElement | HTMLAnchorElement | H const isAnchor = as === 'a'; return ( - <ButtonComponent - className={`${styles.btn} ${typeClass} ${variantClass} ${sizeClass} ${blockClass} ${roundedClass} ${iconOnlyClass} ${className ? className : ''}`} - onClick={onClick} - disabled={disabled} - aria-label={ariaLabel} - href={isAnchor ? href : undefined} - ref={forwardedRef as React.RefObject<any>} - {...props} - > - {iconPosition === 'leading' && icon} - {label && <span>{label}</span>} - {children && <span>{children}</span>} - {iconPosition === 'trailing' && icon} - </ButtonComponent> + <Tooltip> + {tooltip && <TooltipContent>{tooltip}</TooltipContent>} + <TooltipTrigger> + <ButtonComponent + className={`${styles.btn} ${typeClass} ${variantClass} ${sizeClass} ${blockClass} ${roundedClass} ${iconOnlyClass} ${className ? className : ''}`} + onClick={onClick} + disabled={disabled} + aria-label={ariaLabel} + href={isAnchor ? href : undefined} + ref={forwardedRef as React.RefObject<any>} + {...props} + > + {iconPosition === 'leading' && icon} + {label && <span>{label}</span>} + {children && <span>{children}</span>} + {iconPosition === 'trailing' && icon} + </ButtonComponent> + </TooltipTrigger> + </Tooltip> ); }, ); diff --git a/libs/shared/lib/components/buttons/buttons.module.scss b/libs/shared/lib/components/buttons/buttons.module.scss index 32d79309d06afd73451c86dd2e0860ec74a08bbe..a348a6e10fe2c25f8aee3e9be99e191bcbef3cfb 100644 --- a/libs/shared/lib/components/buttons/buttons.module.scss +++ b/libs/shared/lib/components/buttons/buttons.module.scss @@ -47,6 +47,16 @@ line-height: 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 { @apply text-lg h-10 gap-1.5; &.btn-icon-only { 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 83aafa7918952944b7108678579d269c74792d98..ac3cb0637e258b41ca471d130139c09b5f86f1a0 100644 --- a/libs/shared/lib/components/buttons/buttons.module.scss.d.ts +++ b/libs/shared/lib/components/buttons/buttons.module.scss.d.ts @@ -1,6 +1,8 @@ declare const classNames: { readonly btn: 'btn'; readonly 'btn-icon-only': 'btn-icon-only'; + readonly 'btn-2xl': 'btn-2xl'; + readonly 'btn-xl': 'btn-xl'; readonly 'btn-lg': 'btn-lg'; readonly 'btn-md': 'btn-md'; readonly 'btn-sm': 'btn-sm'; diff --git a/libs/shared/lib/components/controls/index.tsx b/libs/shared/lib/components/controls/index.tsx index 3660df5a1d67161217fd98ee9188d84302e2d516..a28c32b4173b03c5ad6c004413f86846f74dfe58 100644 --- a/libs/shared/lib/components/controls/index.tsx +++ b/libs/shared/lib/components/controls/index.tsx @@ -5,5 +5,5 @@ type 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>; } diff --git a/libs/shared/lib/components/icon/index.tsx b/libs/shared/lib/components/icon/index.tsx index c4015c4366e2ab3791a257cf29b8a089cbd2c44f..1deb425d1aff180591b3453c95362c1aadba306f 100644 --- a/libs/shared/lib/components/icon/index.tsx +++ b/libs/shared/lib/components/icon/index.tsx @@ -2,7 +2,7 @@ import React, { ReactElement, ReactNode } from 'react'; import { SVGProps } from 'react'; // Define Sizes and IconProps types -export type Sizes = 12 | 14 | 16 | 20 | 24 | 28 | 32 | 40; +export type Sizes = 12 | 14 | 16 | 20 | 24 | 28 | 32 | 36 | 40; export type IconProps = SVGProps<SVGSVGElement> & { component?: ReactNode | ReactElement<any> | string; size?: Sizes; diff --git a/libs/shared/lib/components/inputs/index.tsx b/libs/shared/lib/components/inputs/index.tsx index db355bb7450ae708bdc49cd22ec26e8344272ea9..9485ddfc88b92b1a10b9e960e2b727ad34b06cb7 100644 --- a/libs/shared/lib/components/inputs/index.tsx +++ b/libs/shared/lib/components/inputs/index.tsx @@ -1,9 +1,10 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import styles from './inputs.module.scss'; import { DropdownTrigger, DropdownContainer, DropdownItem, DropdownItemContainer } from '../dropdowns'; import Info from '../info'; -import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../tooltip'; import { Popover } from '../layout/Popover'; +import { Button } from '../buttons'; type SliderProps = { label: string; @@ -37,23 +38,28 @@ type TextProps = { }; type NumberProps = { - label: string; + label?: string; type: 'number'; size?: 'xs' | 'sm' | 'md' | 'xl'; + variant?: 'primary' | 'secondary' | 'danger' | 'warning' | 'success' | 'info' | 'base'; placeholder?: string; value: number; required?: boolean; errorText?: string; visible?: boolean; disabled?: boolean; - tooltip?: string; + tooltip?: string | React.ReactNode; info?: string; inline?: boolean; validate?: (value: any) => boolean; onChange?: (value: number) => void; + onRevert?: (value: number) => void; onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void; max?: number; min?: number; + className?: string; + containerClassName?: string; + lazy?: boolean; }; type CheckboxProps = { @@ -88,7 +94,7 @@ type DropdownProps = { overrideRender?: React.ReactNode; type: 'dropdown'; size?: 'xs' | 'sm' | 'md' | 'xl'; - tooltip?: string; + tooltip?: string | React.ReactNode; required?: boolean; inline?: boolean; buttonVariant?: 'primary' | 'outline' | 'ghost'; @@ -159,7 +165,7 @@ export const SliderInput = ({ label, value, min, max, step, unit, showValue = tr }; export const TextInput = ({ - label, + label = undefined, placeholder, value = '', size = 'md', @@ -225,55 +231,108 @@ export const NumberInput = ({ inline = false, required = false, visible = true, + variant = 'base', errorText, validate, disabled = false, onChange, + onRevert, tooltip, info, onKeyDown, max, min, + className, + containerClassName, + lazy = false, }: NumberProps) => { 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 ( - <Tooltip> - <TooltipTrigger className={styles['input'] + ' form-control w-full' + (inline ? ' grid grid-cols-2 items-center gap-0.5' : '')}> - <label className="label p-0"> - <span - className={`text-sm text-left truncate 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} placement={'left'} />} - </label> - <input - type="number" - placeholder={placeholder} - 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' - }`} - value={value.toString()} - onChange={(e) => { - if (required && validate) { - setIsValid(validate(e.target.value)); - } - if (onChange) { - onChange(Number(e.target.value)); - } - }} - required={required} - disabled={disabled} - onKeyDown={onKeyDown} - max={max} - min={min} - /> - </TooltipTrigger> - {tooltip && <TooltipContent>{tooltip}</TooltipContent>} - </Tooltip> + <div className={styles['input'] + `${containerClassName ? ` ${containerClassName}` : ''}`}> + <TooltipProvider delayDuration={50}> + <Tooltip> + <TooltipTrigger className={'form-control w-full' + (inline && label ? ' grid grid-cols-2 items-center gap-0.5' : '')}> + {label && ( + <label className="label p-0"> + <span + className={`text-sm text-left truncate 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} placement={'left'} />} + </label> + )} + <div className="relative items-center"> + <input + type="number" + placeholder={placeholder} + 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' + }${className ? ` ${className}` : ''}`} + value={inputValue.toString()} + onChange={(e) => { + if (required && validate) { + setIsValid(validate(e.target.value)); + } + setInputValue(Number(e.target.value)); + if (!lazy && onChange) { + onChange(Number(e.target.value)); + } + }} + required={required} + disabled={disabled} + 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> ); }; diff --git a/libs/shared/lib/components/inputs/inputs.module.scss b/libs/shared/lib/components/inputs/inputs.module.scss index 71c96c83b3f9dfa43d2ed48c6b6b54434bcb02af..12c2c55b9af8b19f77f12ad66dd904678393cdad 100644 --- a/libs/shared/lib/components/inputs/inputs.module.scss +++ b/libs/shared/lib/components/inputs/inputs.module.scss @@ -23,11 +23,16 @@ } .input { - input[class~='xs'] { + input[class~='2xs'] { @apply py-0; @apply px-0; @apply sm:text-2xs; } + input[class~='xs'] { + @apply py-0.5; + @apply px-0.5; + @apply sm:text-xs; + } input[class~='sm'] { @apply py-1; @apply px-1; @@ -37,10 +42,6 @@ @apply py-2; @apply px-3; } - input[class~='md'] { - @apply py-2; - @apply px-3; - } input[class~='xl'] { @apply py-3; @apply px-5; diff --git a/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx b/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx index 3e93ec3dab142e37f48ba3fe3bdf9999893ad42f..2c725687a8d38cad4c7928287d5d7c600a405cf2 100644 --- a/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx +++ b/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx @@ -1,5 +1,6 @@ import { useConfig, + useGraphQueryResult, useQuerybuilderGraph, useQuerybuilderHash, useQuerybuilderSettings, diff --git a/libs/shared/lib/querybuilder/panel/QueryBuilderNav.tsx b/libs/shared/lib/querybuilder/panel/QueryBuilderNav.tsx index d0b39b72d17196b8d5d3856db19e3dea4feeb473..f3110d640b8b1ef6fe5550ee03733cdc6534b806 100644 --- a/libs/shared/lib/querybuilder/panel/QueryBuilderNav.tsx +++ b/libs/shared/lib/querybuilder/panel/QueryBuilderNav.tsx @@ -1,8 +1,8 @@ -import React from 'react'; -import { ControlContainer, TooltipProvider, Tooltip, TooltipTrigger, Button, TooltipContent } from '../../components'; +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 } from '../../data-access'; -import { clearQB } from '../../data-access/store/querybuilderSlice'; +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'; @@ -19,6 +19,9 @@ export type QueryBuilderNavProps = { 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. @@ -169,6 +172,45 @@ export const QueryBuilderNav = (props: QueryBuilderNavProps) => { <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> diff --git a/libs/shared/lib/querybuilder/panel/querysidepanel/QuerySettings.tsx b/libs/shared/lib/querybuilder/panel/querysidepanel/QuerySettings.tsx index d3b36d1d94ce98c95bc4f859483c1a596bdc259d..f6e245459e7a4e780ea282268b4790dd2866fbfe 100644 --- a/libs/shared/lib/querybuilder/panel/querysidepanel/QuerySettings.tsx +++ b/libs/shared/lib/querybuilder/panel/querysidepanel/QuerySettings.tsx @@ -40,16 +40,6 @@ export const QuerySettings = React.forwardRef<HTMLDivElement, {}>((props, ref) = 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 type="number" label="Min Depth Default"