mirror of
https://github.com/datahub-project/datahub.git
synced 2025-10-29 17:59:24 +00:00
feat(components) Consolidate SaaS and OSS component libraries (#14504)
This commit is contained in:
parent
a9fbd6154c
commit
8ade946f76
@ -14,7 +14,7 @@ export const avatarListDefaults: AvatarStackProps = {
|
||||
};
|
||||
|
||||
export const AvatarStack = ({ avatars, size = 'md', showRemainingNumber = true, maxToShow = 4 }: AvatarStackProps) => {
|
||||
if (avatars?.length === 0) return <div>-</div>;
|
||||
if (!avatars?.length) return <div>-</div>;
|
||||
const remainingNumber = avatars.length - maxToShow;
|
||||
const renderAvatarStack = avatars?.slice(0, maxToShow).map((avatar: AvatarItemProps) => (
|
||||
<AvatarContainer key={avatar.name}>
|
||||
|
||||
@ -13,7 +13,7 @@ export interface AvatarItemProps {
|
||||
}
|
||||
|
||||
export type AvatarStackProps = {
|
||||
avatars: AvatarItemProps[];
|
||||
avatars?: AvatarItemProps[];
|
||||
size?: AvatarSizeOptions;
|
||||
showRemainingNumber?: boolean;
|
||||
maxToShow?: number;
|
||||
|
||||
@ -122,17 +122,22 @@ const getButtonColorStyles = (variant: ButtonVariant, color: ColorOptions, theme
|
||||
};
|
||||
|
||||
// Generate color styles for button
|
||||
const getButtonVariantStyles = (variant: ButtonVariant, colorStyles: ColorStyles, color: ColorOptions): CSSObject => {
|
||||
const isViolet = color === 'violet';
|
||||
const violetGradient = `radial-gradient(115.48% 144.44% at 50% -44.44%, var(--buttons-bg-2-for-gradient, #705EE4) 38.97%, var(--buttons-bg, #533FD1) 100%)`;
|
||||
const getButtonVariantStyles = (
|
||||
variant: ButtonVariant,
|
||||
colorStyles: ColorStyles,
|
||||
color: ColorOptions,
|
||||
theme?: Theme,
|
||||
): CSSObject => {
|
||||
const isPrimary = color === 'violet' || color === 'primary';
|
||||
const primaryGradient = `radial-gradient(115.48% 144.44% at 50% -44.44%, ${theme?.styles?.['primary-color-gradient'] || '#705EE4'} 38.97%, ${theme?.styles?.['primary-color'] || '#533FD1'} 100%)`;
|
||||
|
||||
const variantStyles = {
|
||||
filled: {
|
||||
background: isViolet ? violetGradient : colorStyles.bgColor,
|
||||
background: isPrimary ? primaryGradient : colorStyles.bgColor,
|
||||
border: `1px solid ${colorStyles.borderColor}`,
|
||||
color: colorStyles.textColor,
|
||||
'&:hover': {
|
||||
background: isViolet ? violetGradient : colorStyles.hoverBgColor,
|
||||
background: isPrimary ? primaryGradient : colorStyles.hoverBgColor,
|
||||
border: `1px solid ${colorStyles.hoverBgColor}`,
|
||||
boxShadow: shadows.sm,
|
||||
},
|
||||
@ -276,7 +281,7 @@ export const getButtonStyle = (props: ButtonStyleProps): CSSObject => {
|
||||
const colorStyles = getButtonColorStyles(variant, color, theme);
|
||||
|
||||
// Define styles for button
|
||||
const variantStyles = getButtonVariantStyles(variant, colorStyles, color);
|
||||
const variantStyles = getButtonVariantStyles(variant, colorStyles, color, theme);
|
||||
const fontStyles = getButtonFontStyles(size);
|
||||
const radiiStyles = getButtonRadiiStyles(isCircle);
|
||||
const paddingStyles = getButtonPadding(size, hasChildren, isCircle, variant);
|
||||
|
||||
@ -126,6 +126,16 @@ export const states = () => (
|
||||
</GridList>
|
||||
);
|
||||
|
||||
export const sizes = () => (
|
||||
<GridList>
|
||||
<Checkbox label="Extra Small" isChecked size="xs" />
|
||||
<Checkbox label="Small" isChecked size="sm" />
|
||||
<Checkbox label="Medium" isChecked size="md" />
|
||||
<Checkbox label="Large" isChecked size="lg" />
|
||||
<Checkbox label="Extra Large" isChecked size="xl" />
|
||||
</GridList>
|
||||
);
|
||||
|
||||
export const intermediate = () => {
|
||||
return (
|
||||
<GridList>
|
||||
|
||||
@ -21,7 +21,7 @@ export function getCheckboxColor(checked: boolean, error: string, disabled: bool
|
||||
}
|
||||
if (error) return checkboxBackgroundDefault.error;
|
||||
if (checked) return checkboxBackgroundDefault.checked;
|
||||
return mode === 'background' ? checkboxBackgroundDefault.default : colors.gray[500];
|
||||
return mode === 'background' ? checkboxBackgroundDefault.default : colors.gray[1800];
|
||||
}
|
||||
|
||||
export function getCheckboxHoverBackgroundColor(checked: boolean, error: string) {
|
||||
@ -36,7 +36,7 @@ const sizeMap: Record<SizeOptions, string> = {
|
||||
md: '20px',
|
||||
lg: '22px',
|
||||
xl: '24px',
|
||||
inherit: '',
|
||||
inherit: 'inherit',
|
||||
};
|
||||
|
||||
export function getCheckboxSize(size: SizeOptions) {
|
||||
|
||||
@ -79,7 +79,7 @@ export function GraphCard({
|
||||
)}
|
||||
<Text color="gray">{emptyMessage}</Text>
|
||||
{moreInfoModalContent && (
|
||||
<LinkText color="violet" onClick={() => setShowInfoModal(true)}>
|
||||
<LinkText color="primary" onClick={() => setShowInfoModal(true)}>
|
||||
More info
|
||||
</LinkText>
|
||||
)}
|
||||
|
||||
@ -6,6 +6,8 @@ import { IconProps, IconPropsDefaults } from '@components/components/Icon/types'
|
||||
import { getIconComponent, getIconNames } from '@components/components/Icon/utils';
|
||||
import { getColor, getFontSize, getRotationTransform } from '@components/theme/utils';
|
||||
|
||||
import { useCustomTheme } from '@src/customThemeContext';
|
||||
|
||||
export const iconDefaults: IconPropsDefaults = {
|
||||
source: 'material',
|
||||
variant: 'outline',
|
||||
@ -28,6 +30,7 @@ export const Icon = ({
|
||||
...props
|
||||
}: IconProps) => {
|
||||
const { filled, outlined } = getIconNames();
|
||||
const { theme } = useCustomTheme();
|
||||
|
||||
// Return early if no icon is provided
|
||||
if (!icon) return null;
|
||||
@ -60,9 +63,9 @@ export const Icon = ({
|
||||
<IconComponent
|
||||
sx={{
|
||||
fontSize: getFontSize(size),
|
||||
color: getColor(color, colorLevel),
|
||||
color: getColor(color, colorLevel, theme),
|
||||
}}
|
||||
style={{ color: getColor(color, colorLevel) }}
|
||||
style={{ color: getColor(color, colorLevel, theme) }}
|
||||
weight={source === 'phosphor' ? weight : undefined} // Phosphor icons use 'weight' prop
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
@ -36,6 +36,10 @@ const SearchIcon = styled(Icon)`
|
||||
margin-left: 8px;
|
||||
`;
|
||||
|
||||
const ClearIcon = styled(Icon)`
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
export const Input = ({
|
||||
value = inputDefaults.value,
|
||||
setValue = inputDefaults.setValue,
|
||||
@ -55,6 +59,7 @@ export const Input = ({
|
||||
id,
|
||||
inputStyles,
|
||||
inputTestId,
|
||||
onClear,
|
||||
...props
|
||||
}: InputProps) => {
|
||||
// Invalid state is always true if error is present
|
||||
@ -104,6 +109,7 @@ export const Input = ({
|
||||
{warning && <Icon icon="ErrorOutline" color="yellow" size="lg" />}
|
||||
</Tooltip>
|
||||
)}
|
||||
{!!onClear && value && <ClearIcon source="phosphor" icon="X" size="lg" onClick={onClear} />}
|
||||
{isPassword && <Icon onClick={() => setShowPassword(!showPassword)} icon={passwordIcon} size="lg" />}
|
||||
</InputContainer>
|
||||
{invalid && error && !errorOnHover && <ErrorMessage>{error}</ErrorMessage>}
|
||||
|
||||
@ -22,4 +22,5 @@ export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
styles?: React.CSSProperties;
|
||||
inputStyles?: React.CSSProperties;
|
||||
inputTestId?: string;
|
||||
onClear?: () => void;
|
||||
}
|
||||
|
||||
@ -77,13 +77,13 @@ function Render({ buttons, buttonExamples, content, ...props }: ModalWithExample
|
||||
buttonExamples?.map((text) => {
|
||||
switch (text) {
|
||||
case 'Cancel':
|
||||
return { text, variant: 'text', onClick: () => setIsOpen(false) };
|
||||
return { text, variant: 'text', onClick: () => setIsOpen(false), key: 'cancel' };
|
||||
case 'Propose':
|
||||
// TODO: Replace with secondary variant once it's supported
|
||||
return { text, variant: 'outline', onClick: () => setIsOpen(false) };
|
||||
return { text, variant: 'outline', onClick: () => setIsOpen(false), key: 'propose' };
|
||||
case 'Submit':
|
||||
default:
|
||||
return { text, variant: 'filled', onClick: () => setIsOpen(false) };
|
||||
return { text, variant: 'filled', onClick: () => setIsOpen(false), key: 'submit' };
|
||||
}
|
||||
}) || [];
|
||||
|
||||
@ -91,7 +91,7 @@ function Render({ buttons, buttonExamples, content, ...props }: ModalWithExample
|
||||
<>
|
||||
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
|
||||
{isOpen && (
|
||||
<Modal {...props} buttons={[...buttons, ...exampleButtons]} onCancel={() => setIsOpen(false)}>
|
||||
<Modal {...props} buttons={[...(buttons || []), ...exampleButtons]} onCancel={() => setIsOpen(false)}>
|
||||
{content && <Text color="gray">{content}</Text>}
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
@ -62,8 +62,8 @@ export interface ModalButton extends ButtonProps {
|
||||
}
|
||||
|
||||
export interface ModalProps {
|
||||
buttons: ModalButton[];
|
||||
title: string;
|
||||
buttons?: ModalButton[];
|
||||
title: React.ReactNode;
|
||||
subtitle?: string;
|
||||
titlePill?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
@ -105,7 +105,7 @@ export function Modal({
|
||||
</HeaderContainer>
|
||||
}
|
||||
footer={
|
||||
!!buttons.length && (
|
||||
!!buttons?.length && (
|
||||
<ButtonsContainer>
|
||||
{buttons.map(({ text, variant, onClick, key, buttonDataTestId, ...buttonProps }, index) => (
|
||||
<Button
|
||||
|
||||
@ -50,6 +50,7 @@ export function Pill({
|
||||
customIconRenderer,
|
||||
showLabel,
|
||||
className,
|
||||
iconSource,
|
||||
}: PillProps) {
|
||||
if (!SUPPORTED_CONFIGURATIONS[variant].includes(color)) {
|
||||
console.debug(`Unsupported configuration for Pill: variant=${variant}, color=${color}`);
|
||||
@ -72,11 +73,11 @@ export function Pill({
|
||||
>
|
||||
{customIconRenderer
|
||||
? customIconRenderer()
|
||||
: leftIcon && <Icon icon={leftIcon} size={size} onClick={onClickLeftIcon} />}
|
||||
: leftIcon && <Icon icon={leftIcon} size={size} onClick={onClickLeftIcon} source={iconSource} />}
|
||||
<PillText style={customStyle}>{label}</PillText>
|
||||
{rightIcon && (
|
||||
<Button style={{ padding: 0 }} variant="text" onClick={onClickRightIcon}>
|
||||
<Icon icon={rightIcon} size={size} />
|
||||
<Icon icon={rightIcon} size={size} source={iconSource} />
|
||||
</Button>
|
||||
)}
|
||||
</PillContainer>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { HTMLAttributes } from 'react';
|
||||
|
||||
import { IconSource } from '@src/alchemy-components/components/Icon/types';
|
||||
import { ColorOptions, PillVariantOptions, SizeOptions } from '@src/alchemy-components/theme/config';
|
||||
import { Theme } from '@src/conf/theme/types';
|
||||
|
||||
@ -16,6 +17,7 @@ export interface PillProps extends Partial<PillPropsDefaults>, Omit<HTMLAttribut
|
||||
color?: ColorOptions;
|
||||
rightIcon?: string;
|
||||
leftIcon?: string;
|
||||
iconSource?: IconSource;
|
||||
customStyle?: React.CSSProperties;
|
||||
showLabel?: boolean;
|
||||
customIconRenderer?: () => void;
|
||||
|
||||
@ -25,7 +25,7 @@ function getPillColorStyles(variant: PillVariantOptions, color: ColorOptions, th
|
||||
}
|
||||
|
||||
return {
|
||||
primaryColor: getColor(color, 500, theme),
|
||||
primaryColor: getColor(color, 700, theme),
|
||||
bgColor: color === 'gray' ? getColor(color, 100, theme) : getColor(color, 0, theme),
|
||||
hoverColor: color === 'gray' ? getColor(color, 100, theme) : getColor(color, 1100, theme),
|
||||
borderColor: getColor('gray', 1800, theme),
|
||||
|
||||
@ -4,7 +4,7 @@ import { getRadioBorderColor, getRadioCheckmarkColor } from '@components/compone
|
||||
import { formLabelTextStyles } from '@components/components/commonStyles';
|
||||
import { borders, colors, radius, spacing } from '@components/theme';
|
||||
|
||||
export const RadioWrapper = styled.div<{ disabled: boolean; error: string }>(({ disabled, error }) => ({
|
||||
export const RadioWrapper = styled.div<{ disabled: boolean; error: string }>(({ disabled, error, theme }) => ({
|
||||
position: 'relative',
|
||||
margin: '20px',
|
||||
width: '20px',
|
||||
@ -19,7 +19,7 @@ export const RadioWrapper = styled.div<{ disabled: boolean; error: string }>(({
|
||||
cursor: !disabled ? 'pointer' : 'none',
|
||||
transition: 'border 0.3s ease, outline 0.3s ease',
|
||||
'&:hover': {
|
||||
border: `${borders['2px']} ${!disabled && !error ? colors.violet[500] : getRadioBorderColor(disabled, error)}`,
|
||||
border: `${borders['2px']} ${!disabled && !error ? theme.styles['primary-color'] : getRadioBorderColor(disabled, error)}`,
|
||||
outline: !disabled && !error ? `${borders['2px']} ${colors.gray[200]}` : 'none',
|
||||
},
|
||||
}));
|
||||
@ -45,7 +45,7 @@ export const Required = styled.span({
|
||||
});
|
||||
|
||||
export const RadioHoverState = styled.div({
|
||||
border: `${borders['2px']} ${colors.violet[500]}`,
|
||||
border: `${borders['2px']} ${(props) => props.theme.styles['primary-color']}`,
|
||||
width: 'calc(100% - -3px)',
|
||||
height: 'calc(100% - -3px)',
|
||||
display: 'flex',
|
||||
|
||||
@ -23,22 +23,30 @@ export const SearchBar = forwardRef<InputRef, SearchBarProps & Omit<InputProps,
|
||||
height = searchBarDefaults.height,
|
||||
allowClear = searchBarDefaults.allowClear,
|
||||
clearIcon,
|
||||
forceUncontrolled = false,
|
||||
onCompositionStart,
|
||||
onCompositionEnd,
|
||||
onChange,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
// Override value handling when forceUncontrolled is true
|
||||
const inputValue = forceUncontrolled ? undefined : value;
|
||||
|
||||
return (
|
||||
<StyledSearchBar
|
||||
placeholder={placeholder}
|
||||
onChange={(e) => onChange?.(e.target.value, e)}
|
||||
value={value}
|
||||
value={inputValue}
|
||||
prefix={<Icon icon="MagnifyingGlass" source="phosphor" />}
|
||||
allowClear={clearIcon ? allowClear && { clearIcon } : allowClear}
|
||||
$width={width}
|
||||
$height={height}
|
||||
data-testid="search-bar-input"
|
||||
ref={ref}
|
||||
onCompositionStart={onCompositionStart}
|
||||
onCompositionEnd={onCompositionEnd}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -11,15 +11,40 @@ export const StyledSearchBar = styled(Input)<{ $width?: string; $height?: string
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
border: 1px solid ${colors.gray[100]};
|
||||
box-shadow: 0px 1px 2px 0px rgba(33, 23, 95, 0.07);
|
||||
transition: all 0.1s ease;
|
||||
|
||||
&.ant-input-affix-wrapper {
|
||||
border: 1px solid ${colors.gray[100]};
|
||||
|
||||
&:not(.ant-input-affix-wrapper-disabled) {
|
||||
&:hover {
|
||||
border-color: ${colors.gray[100]};
|
||||
}
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&-focused {
|
||||
border-color: ${(props) => props.theme.styles['primary-color']};
|
||||
box-shadow: 0px 0px 0px 2px ${colors.violet[100]};
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
color: ${colors.gray[600]};
|
||||
font-size: ${typography.fontSizes.md} !important;
|
||||
background-color: transparent;
|
||||
|
||||
&::placeholder {
|
||||
color: ${colors.gray[400]};
|
||||
}
|
||||
}
|
||||
|
||||
.ant-input-prefix {
|
||||
width: 20px;
|
||||
color: ${colors.gray[1800]};
|
||||
color: ${colors.gray[400]};
|
||||
margin-right: 4px;
|
||||
|
||||
svg {
|
||||
height: 16px;
|
||||
@ -33,4 +58,9 @@ export const StyledSearchBar = styled(Input)<{ $width?: string; $height?: string
|
||||
border-color: ${({ theme }) => getColor('primary', 300, theme)} !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.ant-input-affix-wrapper-focused {
|
||||
border-color: ${(props) => props.theme.styles['primary-color']};
|
||||
box-shadow: 0px 0px 0px 2px ${colors.violet[100]};
|
||||
}
|
||||
`;
|
||||
|
||||
@ -10,4 +10,7 @@ export interface SearchBarProps {
|
||||
clearIcon?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
suffix?: React.ReactNode;
|
||||
forceUncontrolled?: boolean;
|
||||
onCompositionStart?: React.CompositionEventHandler<HTMLInputElement>;
|
||||
onCompositionEnd?: React.CompositionEventHandler<HTMLInputElement>;
|
||||
}
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import useNestedSelectOptionChildren from '@components/components/Select/Nested/useNestedSelectOptionChildren';
|
||||
|
||||
const option1 = { value: '1', label: '1', isParent: true };
|
||||
const option2 = { value: '5', label: '5', isParent: true };
|
||||
const children1 = [{ value: '2', label: '2' }, { value: '3', label: '3' }, { value: '4', label: '4' }, option2];
|
||||
const children2 = [
|
||||
{ value: '6', label: '6' },
|
||||
{ value: '7', label: '7' },
|
||||
{ value: '8', label: '8' },
|
||||
];
|
||||
|
||||
const parentValueToOptions = {
|
||||
[option1.value]: children1,
|
||||
[option2.value]: children2,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
option: option1,
|
||||
parentValueToOptions,
|
||||
areParentsSelectable: true,
|
||||
addOptions: () => {},
|
||||
};
|
||||
|
||||
describe('useNestedSelectOptionChildren', () => {
|
||||
it('should return props properly when parents are selectable', () => {
|
||||
const { result } = renderHook(() => useNestedSelectOptionChildren(defaultProps));
|
||||
|
||||
const { children, selectableChildren, directChildren } = result.current;
|
||||
|
||||
expect(children).toMatchObject([...children1, ...children2]);
|
||||
expect(selectableChildren).toMatchObject([...children1, ...children2]);
|
||||
expect(directChildren).toMatchObject(children1);
|
||||
});
|
||||
|
||||
it('should return props properly when parents are not selectable', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useNestedSelectOptionChildren({ ...defaultProps, areParentsSelectable: false }),
|
||||
);
|
||||
|
||||
const { children, selectableChildren, directChildren } = result.current;
|
||||
|
||||
expect(children).toMatchObject([...children1, ...children2]);
|
||||
expect(selectableChildren).toMatchObject([...children1, ...children2].filter((o) => o.value !== option2.value));
|
||||
expect(directChildren).toMatchObject(children1);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,220 @@
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import useNestedOption from '@components/components/Select/Nested/useSelectOption';
|
||||
|
||||
const option = { value: '1', label: '1', isParent: true };
|
||||
const children = [
|
||||
{ value: '2', label: '2' },
|
||||
{ value: '3', label: '3' },
|
||||
{ value: '4', label: '4' },
|
||||
];
|
||||
|
||||
const defaultProps = {
|
||||
selectedOptions: [],
|
||||
option,
|
||||
children,
|
||||
selectableChildren: children,
|
||||
areParentsSelectable: true,
|
||||
implicitlySelectChildren: false,
|
||||
addOptions: () => {},
|
||||
removeOptions: () => {},
|
||||
setSelectedOptions: () => {},
|
||||
handleOptionChange: () => {},
|
||||
};
|
||||
|
||||
describe('useSelectChildren - areParentsSelectable is true', () => {
|
||||
it('should return props properly when parent is not selected and no children are selected', () => {
|
||||
const mockAddOptions = vi.fn();
|
||||
const { result } = renderHook(() =>
|
||||
useNestedOption({
|
||||
...defaultProps,
|
||||
addOptions: mockAddOptions,
|
||||
}),
|
||||
);
|
||||
|
||||
const { selectOption, isPartialSelected, isParentMissingChildren, isSelected, isImplicitlySelected } =
|
||||
result.current;
|
||||
|
||||
expect(isPartialSelected).toBe(false);
|
||||
expect(isParentMissingChildren).toBe(false);
|
||||
expect(isSelected).toBe(false);
|
||||
expect(isImplicitlySelected).toBe(false);
|
||||
selectOption();
|
||||
expect(mockAddOptions).toHaveBeenCalledWith([option, ...children]);
|
||||
});
|
||||
|
||||
it('should return props properly when parent is selected and no children are selected', () => {
|
||||
const mockAddOptions = vi.fn();
|
||||
const { result } = renderHook(() =>
|
||||
useNestedOption({
|
||||
...defaultProps,
|
||||
selectedOptions: [option],
|
||||
addOptions: mockAddOptions,
|
||||
}),
|
||||
);
|
||||
|
||||
const { selectOption, isPartialSelected, isParentMissingChildren, isSelected, isImplicitlySelected } =
|
||||
result.current;
|
||||
|
||||
expect(isPartialSelected).toBe(true);
|
||||
expect(isParentMissingChildren).toBe(false);
|
||||
expect(isSelected).toBe(true);
|
||||
expect(isImplicitlySelected).toBe(false);
|
||||
selectOption();
|
||||
expect(mockAddOptions).toHaveBeenCalledWith([option, ...children]);
|
||||
});
|
||||
|
||||
it('should return props properly when parent is not selected and some children are selected', () => {
|
||||
const mockAddOptions = vi.fn();
|
||||
const { result } = renderHook(() =>
|
||||
useNestedOption({
|
||||
...defaultProps,
|
||||
selectedOptions: [children[0]],
|
||||
addOptions: mockAddOptions,
|
||||
}),
|
||||
);
|
||||
|
||||
const { selectOption, isPartialSelected, isParentMissingChildren, isSelected, isImplicitlySelected } =
|
||||
result.current;
|
||||
|
||||
expect(isPartialSelected).toBe(true);
|
||||
expect(isParentMissingChildren).toBe(false);
|
||||
expect(isSelected).toBe(false);
|
||||
expect(isImplicitlySelected).toBe(false);
|
||||
selectOption();
|
||||
expect(mockAddOptions).toHaveBeenCalledWith([option, ...children]);
|
||||
});
|
||||
|
||||
it('should return props properly when parent and children are selected', () => {
|
||||
const mockRemoveOptions = vi.fn();
|
||||
const { result } = renderHook(() =>
|
||||
useNestedOption({
|
||||
...defaultProps,
|
||||
selectedOptions: [option, ...children],
|
||||
removeOptions: mockRemoveOptions,
|
||||
}),
|
||||
);
|
||||
|
||||
const { selectOption, isPartialSelected, isParentMissingChildren, isSelected, isImplicitlySelected } =
|
||||
result.current;
|
||||
|
||||
expect(isPartialSelected).toBe(false);
|
||||
expect(isParentMissingChildren).toBe(false);
|
||||
expect(isSelected).toBe(true);
|
||||
expect(isImplicitlySelected).toBe(false);
|
||||
selectOption();
|
||||
expect(mockRemoveOptions).toHaveBeenCalledWith([option, ...children]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useSelectChildren - areParentsSelectable is false', () => {
|
||||
it('should return props properly when parent is not selected and no children are selected', () => {
|
||||
const mockAddOptions = vi.fn();
|
||||
const { result } = renderHook(() =>
|
||||
useNestedOption({
|
||||
...defaultProps,
|
||||
areParentsSelectable: false,
|
||||
addOptions: mockAddOptions,
|
||||
}),
|
||||
);
|
||||
|
||||
const { selectOption, isPartialSelected, isParentMissingChildren, isSelected, isImplicitlySelected } =
|
||||
result.current;
|
||||
|
||||
expect(isPartialSelected).toBe(false);
|
||||
expect(isParentMissingChildren).toBe(false);
|
||||
expect(isSelected).toBe(false);
|
||||
expect(isImplicitlySelected).toBe(false);
|
||||
selectOption();
|
||||
expect(mockAddOptions).toHaveBeenCalledWith(children);
|
||||
});
|
||||
|
||||
it('should return props properly when some children are selected', () => {
|
||||
const mockAddOptions = vi.fn();
|
||||
const { result } = renderHook(() =>
|
||||
useNestedOption({
|
||||
...defaultProps,
|
||||
areParentsSelectable: false,
|
||||
selectedOptions: [children[0]],
|
||||
addOptions: mockAddOptions,
|
||||
}),
|
||||
);
|
||||
|
||||
const { selectOption, isPartialSelected, isParentMissingChildren, isSelected, isImplicitlySelected } =
|
||||
result.current;
|
||||
|
||||
expect(isPartialSelected).toBe(true);
|
||||
expect(isParentMissingChildren).toBe(false);
|
||||
expect(isSelected).toBe(false);
|
||||
expect(isImplicitlySelected).toBe(false);
|
||||
selectOption();
|
||||
expect(mockAddOptions).toHaveBeenCalledWith(children);
|
||||
});
|
||||
|
||||
it('should return props properly when all children are selected', () => {
|
||||
const mockRemoveOptions = vi.fn();
|
||||
const { result } = renderHook(() =>
|
||||
useNestedOption({
|
||||
...defaultProps,
|
||||
areParentsSelectable: false,
|
||||
selectedOptions: [option, ...children],
|
||||
removeOptions: mockRemoveOptions,
|
||||
}),
|
||||
);
|
||||
|
||||
const { selectOption, isPartialSelected, isParentMissingChildren, isSelected, isImplicitlySelected } =
|
||||
result.current;
|
||||
|
||||
expect(isPartialSelected).toBe(false);
|
||||
expect(isParentMissingChildren).toBe(false);
|
||||
expect(isSelected).toBe(true);
|
||||
expect(isImplicitlySelected).toBe(false);
|
||||
selectOption();
|
||||
expect(mockRemoveOptions).toHaveBeenCalledWith([option, ...children]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useSelectChildren - areParentsSelectable is true & implicitlySelectChildren is true', () => {
|
||||
it('should return props properly when parent is not selected and no children are selected', () => {
|
||||
const mockSetSelectedOptions = vi.fn();
|
||||
const { result } = renderHook(() =>
|
||||
useNestedOption({
|
||||
...defaultProps,
|
||||
implicitlySelectChildren: true,
|
||||
setSelectedOptions: mockSetSelectedOptions,
|
||||
}),
|
||||
);
|
||||
|
||||
const { selectOption, isPartialSelected, isParentMissingChildren, isSelected, isImplicitlySelected } =
|
||||
result.current;
|
||||
|
||||
expect(isPartialSelected).toBe(false);
|
||||
expect(isParentMissingChildren).toBe(false);
|
||||
expect(isSelected).toBe(false);
|
||||
expect(isImplicitlySelected).toBe(false);
|
||||
selectOption();
|
||||
expect(mockSetSelectedOptions).toHaveBeenCalledWith([option]);
|
||||
});
|
||||
|
||||
it('should return props properly when parent is selected and no children are selected', () => {
|
||||
const mockRemoveOptions = vi.fn();
|
||||
const { result } = renderHook(() =>
|
||||
useNestedOption({
|
||||
...defaultProps,
|
||||
implicitlySelectChildren: true,
|
||||
selectedOptions: [option],
|
||||
removeOptions: mockRemoveOptions,
|
||||
}),
|
||||
);
|
||||
|
||||
const { selectOption, isPartialSelected, isParentMissingChildren, isSelected, isImplicitlySelected } =
|
||||
result.current;
|
||||
|
||||
expect(isPartialSelected).toBe(true);
|
||||
expect(isParentMissingChildren).toBe(false);
|
||||
expect(isSelected).toBe(true);
|
||||
expect(isImplicitlySelected).toBe(false);
|
||||
selectOption();
|
||||
expect(mockRemoveOptions).toHaveBeenCalledWith([option]);
|
||||
});
|
||||
});
|
||||
@ -187,6 +187,12 @@ const meta: Meta = {
|
||||
defaultValue: { summary: 'undefined' },
|
||||
},
|
||||
},
|
||||
emptyState: {
|
||||
description: 'Custom empty state component to render when no options are available',
|
||||
table: {
|
||||
defaultValue: { summary: 'undefined' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Define defaults
|
||||
|
||||
@ -16,9 +16,10 @@ interface DropdownSearchBarProps {
|
||||
value?: string;
|
||||
size?: SelectSizeOptions;
|
||||
onChange?: (value: string) => void;
|
||||
onClear?: () => void;
|
||||
}
|
||||
|
||||
export default function DropdownSearchBar({ placeholder, value, size, onChange }: DropdownSearchBarProps) {
|
||||
export default function DropdownSearchBar({ placeholder, value, size, onChange, onClear }: DropdownSearchBarProps) {
|
||||
return (
|
||||
<SearchInputContainer>
|
||||
<Input
|
||||
@ -29,6 +30,7 @@ export default function DropdownSearchBar({ placeholder, value, size, onChange }
|
||||
value={value}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
style={{ fontSize: size || 'md' }}
|
||||
onClear={onClear}
|
||||
/>
|
||||
</SearchInputContainer>
|
||||
);
|
||||
|
||||
@ -49,8 +49,7 @@ export const useEntityOperations = ({
|
||||
useGetAutoCompleteResultsLazyQuery();
|
||||
|
||||
// Handles search input for entity autocomplete
|
||||
const handleSearchEntities = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = e.target;
|
||||
const handleSearchEntities = (value: string) => {
|
||||
setSearchText(value);
|
||||
};
|
||||
|
||||
|
||||
@ -30,7 +30,6 @@ export const BaseTable = styled.table({
|
||||
|
||||
export const TableHeader = styled.thead({
|
||||
backgroundColor: colors.gray[1500],
|
||||
boxShadow: '0px 1px 1px rgba(0, 0, 0, 0.1)',
|
||||
borderRadius: radius.lg,
|
||||
borderBottom: `1px solid ${colors.gray[1400]}`,
|
||||
position: 'sticky',
|
||||
|
||||
@ -14,30 +14,49 @@ export const useRowSelection = <T>(
|
||||
[data, getCheckboxProps],
|
||||
);
|
||||
|
||||
const isSelectAll = useMemo(
|
||||
const selectableRowsKeys = useMemo(
|
||||
() =>
|
||||
data.length > 0 &&
|
||||
selectedRowKeys.length !== 0 &&
|
||||
selectedRowKeys.length === data.length - disabledRows.length,
|
||||
[selectedRowKeys, data, disabledRows],
|
||||
rowSelection
|
||||
? data
|
||||
.filter((row) => !rowSelection.getCheckboxProps?.(row).disabled)
|
||||
.map((row, index) => getRowKey(row, index, rowKey))
|
||||
: [],
|
||||
[data, rowSelection, rowKey],
|
||||
);
|
||||
|
||||
const isSelectAll = useMemo(
|
||||
() => selectableRowsKeys.length > 0 && selectableRowsKeys.every((key) => selectedRowKeys.includes(key)),
|
||||
[selectedRowKeys, selectableRowsKeys],
|
||||
);
|
||||
|
||||
const isSelectAllDisabled = useMemo(() => data.length === disabledRows.length, [data, disabledRows]);
|
||||
|
||||
const isIntermediate = useMemo(
|
||||
() => selectedRowKeys.length > 0 && selectedRowKeys.length < data.length - disabledRows.length,
|
||||
[selectedRowKeys, data, disabledRows],
|
||||
() =>
|
||||
selectableRowsKeys.some((key) => selectedRowKeys.includes(key)) &&
|
||||
!selectableRowsKeys.every((key) => selectedRowKeys.includes(key)),
|
||||
[selectedRowKeys, selectableRowsKeys],
|
||||
);
|
||||
|
||||
const handleSelectAll = useCallback(() => {
|
||||
if (!rowSelection) return;
|
||||
const newSelectedKeys = isSelectAll
|
||||
? []
|
||||
: data
|
||||
.filter((row) => !rowSelection.getCheckboxProps?.(row).disabled)
|
||||
.map((row, index) => getRowKey(row, index, rowKey));
|
||||
onChange?.(newSelectedKeys, data);
|
||||
}, [rowSelection, isSelectAll, data, onChange, rowKey]);
|
||||
|
||||
const currentPageKeys = selectableRowsKeys;
|
||||
let newSelectedKeys;
|
||||
|
||||
if (isSelectAll) {
|
||||
// Deselect only the current page's rows
|
||||
newSelectedKeys = selectedRowKeys.filter((key) => !currentPageKeys.includes(key));
|
||||
} else {
|
||||
// Add current page's rows to existing selection (removing duplicates)
|
||||
const keysToAdd = currentPageKeys.filter((key) => !selectedRowKeys.includes(key));
|
||||
newSelectedKeys = [...selectedRowKeys, ...keysToAdd];
|
||||
}
|
||||
|
||||
const selectedRows = data.filter((row, idx) => newSelectedKeys.includes(getRowKey(row, idx, rowKey)));
|
||||
|
||||
onChange?.(newSelectedKeys, selectedRows);
|
||||
}, [rowSelection, isSelectAll, selectedRowKeys, selectableRowsKeys, data, onChange, rowKey]);
|
||||
|
||||
const handleRowSelect = (record: T, index: number) => {
|
||||
if (!rowSelection) return;
|
||||
|
||||
@ -70,6 +70,7 @@ const meta = {
|
||||
argTypes: {
|
||||
tabs: {
|
||||
description: 'The tabs you want to display',
|
||||
control: 'object',
|
||||
},
|
||||
selectedTab: {
|
||||
description: 'A controlled pieces of state for which tab is selected. This is the key of the tab',
|
||||
@ -90,6 +91,13 @@ const meta = {
|
||||
getCurrentUrl: {
|
||||
description: 'A custom function to get the current URL. Defaults to window.location.pathname',
|
||||
},
|
||||
secondary: {
|
||||
description: 'Whether to render the tabs in a secondary style',
|
||||
},
|
||||
styleOptions: {
|
||||
description: 'Style options for the tabs component',
|
||||
control: 'object',
|
||||
},
|
||||
scrollToTopOnChange: {
|
||||
description: 'Whether to scroll to the top of the tabs container when switching tabs',
|
||||
control: { type: 'boolean' },
|
||||
@ -104,11 +112,13 @@ const meta = {
|
||||
control: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
|
||||
// Args for the story
|
||||
|
||||
args: {
|
||||
tabs: exampleTabs,
|
||||
styleOptions: {
|
||||
navMarginBottom: 16,
|
||||
navMarginTop: 0,
|
||||
containerHeight: 'auto',
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Tabs>;
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ import styled from 'styled-components';
|
||||
import { Pill } from '@components/components/Pills';
|
||||
import { Tooltip } from '@components/components/Tooltip';
|
||||
|
||||
import { ErrorBoundary } from '@app/sharedV2/ErrorHandling/ErrorBoundary';
|
||||
import { colors } from '@src/alchemy-components/theme';
|
||||
|
||||
const ScrollableTabsContainer = styled.div<{ $maxHeight?: string }>`
|
||||
@ -14,13 +15,24 @@ const ScrollableTabsContainer = styled.div<{ $maxHeight?: string }>`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const StyledTabs = styled(AntTabs)<{
|
||||
const StyledTabsPrimary = styled(AntTabs)<{
|
||||
$navMarginBottom?: number;
|
||||
$navMarginTop?: number;
|
||||
$containerHeight?: 'full' | 'auto';
|
||||
$addPaddingLeft?: boolean;
|
||||
$hideTabsHeader: boolean;
|
||||
$scrollable?: boolean;
|
||||
$stickyHeader?: boolean;
|
||||
}>`
|
||||
${({ $scrollable }) => !$scrollable && 'flex: 1;'}
|
||||
${({ $scrollable, $containerHeight }) => {
|
||||
if (!$scrollable) {
|
||||
if ($containerHeight === 'full') {
|
||||
return 'height: 100%;';
|
||||
}
|
||||
return 'flex: 1;';
|
||||
}
|
||||
return '';
|
||||
}}
|
||||
${({ $scrollable }) => !$scrollable && 'overflow: hidden;'}
|
||||
|
||||
.ant-tabs-tab {
|
||||
@ -79,7 +91,85 @@ const StyledTabs = styled(AntTabs)<{
|
||||
}
|
||||
|
||||
.ant-tabs-nav {
|
||||
margin-bottom: 0px;
|
||||
margin-bottom: ${(props) => props.$navMarginBottom ?? 8}px;
|
||||
margin-top: ${(props) => props.$navMarginTop ?? 0}px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTabsSecondary = styled(AntTabs)<{
|
||||
$navMarginBottom?: number;
|
||||
$navMarginTop?: number;
|
||||
$containerHeight?: 'full' | 'auto';
|
||||
$addPaddingLeft?: boolean;
|
||||
$hideTabsHeader: boolean;
|
||||
$scrollable?: boolean;
|
||||
$stickyHeader?: boolean;
|
||||
}>`
|
||||
${(props) =>
|
||||
props.$containerHeight === 'full'
|
||||
? `
|
||||
height: 100%;
|
||||
`
|
||||
: `
|
||||
flex: 1;
|
||||
`}
|
||||
overflow: hidden;
|
||||
|
||||
.ant-tabs-tab {
|
||||
padding: 8px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: ${colors.gray[600]};
|
||||
}
|
||||
|
||||
${({ $addPaddingLeft }) =>
|
||||
$addPaddingLeft
|
||||
? `
|
||||
.ant-tabs-tab {
|
||||
margin-left: 8px;
|
||||
}
|
||||
`
|
||||
: `
|
||||
.ant-tabs-tab + .ant-tabs-tab {
|
||||
margin-left: 8px;
|
||||
}
|
||||
`}
|
||||
|
||||
${({ $hideTabsHeader }) =>
|
||||
$hideTabsHeader &&
|
||||
`
|
||||
.ant-tabs-nav {
|
||||
display: none;
|
||||
}
|
||||
`}
|
||||
|
||||
.ant-tabs-tab-active {
|
||||
background-color: ${(props) => props.theme.styles['primary-color-light']}80;
|
||||
}
|
||||
|
||||
.ant-tabs-tab-active .ant-tabs-tab-btn {
|
||||
color: ${(props) => props.theme.styles['primary-color']};
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ant-tabs-ink-bar {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ant-tabs-tabpane {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-tabs-nav {
|
||||
margin-bottom: ${(props) => props.$navMarginBottom ?? 0}px;
|
||||
margin-top: ${(props) => props.$navMarginTop ?? 0}px;
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
${({ $stickyHeader }) =>
|
||||
@ -126,7 +216,7 @@ function TabView({ tab }: { tab: Tab }) {
|
||||
<Tooltip title={tab.tooltip}>
|
||||
<TabViewWrapper id={tab.id} $disabled={tab.disabled} data-testid={tab.dataTestId}>
|
||||
{tab.name}
|
||||
{!!tab.count && <Pill label={`${tab.count}`} size="xs" color="violet" />}
|
||||
{!!tab.count && <Pill label={`${tab.count}`} size="xs" color="primary" />}
|
||||
</TabViewWrapper>
|
||||
</Tooltip>
|
||||
);
|
||||
@ -152,6 +242,12 @@ export interface Props {
|
||||
onUrlChange?: (url: string) => void;
|
||||
defaultTab?: string;
|
||||
getCurrentUrl?: () => string;
|
||||
secondary?: boolean;
|
||||
styleOptions?: {
|
||||
containerHeight?: 'full' | 'auto';
|
||||
navMarginBottom?: number;
|
||||
navMarginTop?: number;
|
||||
};
|
||||
addPaddingLeft?: boolean;
|
||||
hideTabsHeader?: boolean;
|
||||
scrollToTopOnChange?: boolean;
|
||||
@ -167,6 +263,8 @@ export function Tabs({
|
||||
onUrlChange = (url) => window.history.replaceState({}, '', url),
|
||||
defaultTab,
|
||||
getCurrentUrl = () => window.location.pathname,
|
||||
secondary,
|
||||
styleOptions,
|
||||
addPaddingLeft,
|
||||
hideTabsHeader,
|
||||
scrollToTopOnChange = false,
|
||||
@ -208,6 +306,8 @@ export function Tabs({
|
||||
}
|
||||
}, [getCurrentUrl, onChange, onUrlChange, selectedTab, urlMap, urlToTabMap, defaultTab]);
|
||||
|
||||
const StyledTabs = secondary ? StyledTabsSecondary : StyledTabsPrimary;
|
||||
|
||||
const tabsContent = (
|
||||
<StyledTabs
|
||||
activeKey={selectedTab}
|
||||
@ -217,6 +317,9 @@ export function Tabs({
|
||||
onUrlChange(urlMap[key]);
|
||||
}
|
||||
}}
|
||||
$navMarginBottom={styleOptions?.navMarginBottom}
|
||||
$navMarginTop={styleOptions?.navMarginTop}
|
||||
$containerHeight={styleOptions?.containerHeight}
|
||||
$addPaddingLeft={addPaddingLeft}
|
||||
$hideTabsHeader={!!hideTabsHeader}
|
||||
$scrollable={scrollToTopOnChange}
|
||||
@ -225,7 +328,9 @@ export function Tabs({
|
||||
{tabs.map((tab) => {
|
||||
return (
|
||||
<TabPane tab={<TabView tab={tab} />} key={tab.key} disabled={tab.disabled}>
|
||||
{tab.component}
|
||||
<ErrorBoundary resetKeys={[tab.key]} variant="tab">
|
||||
{tab.component}
|
||||
</ErrorBoundary>
|
||||
</TabPane>
|
||||
);
|
||||
})}
|
||||
|
||||
@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
CardContainer,
|
||||
SubTitle,
|
||||
SubTitleContainer,
|
||||
Title,
|
||||
TitleContainer,
|
||||
} from '@components/components/Card/components';
|
||||
import { Switch } from '@components/components/Switch';
|
||||
import { Header } from '@components/components/ToggleCard/components';
|
||||
import { ToggleCardProps } from '@components/components/ToggleCard/types';
|
||||
|
||||
export const ToggleCard = ({
|
||||
title,
|
||||
subTitle,
|
||||
value,
|
||||
disabled,
|
||||
onToggle,
|
||||
toggleDataTestId,
|
||||
children,
|
||||
}: ToggleCardProps) => {
|
||||
return (
|
||||
<CardContainer>
|
||||
<Header>
|
||||
<TitleContainer>
|
||||
<Title>{title}</Title>
|
||||
<SubTitleContainer>
|
||||
<SubTitle>{subTitle}</SubTitle>
|
||||
</SubTitleContainer>
|
||||
</TitleContainer>
|
||||
<Switch
|
||||
disabled={disabled}
|
||||
isChecked={value}
|
||||
onChange={(e) => onToggle(e.target.checked)}
|
||||
colorScheme="primary"
|
||||
label=""
|
||||
labelStyle={{ display: 'none' }}
|
||||
data-testid={toggleDataTestId}
|
||||
/>
|
||||
</Header>
|
||||
{children}
|
||||
</CardContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Header = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
@ -0,0 +1 @@
|
||||
export { ToggleCard } from './ToggleCard';
|
||||
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
export interface ToggleCardProps {
|
||||
title: string;
|
||||
value: boolean;
|
||||
disabled?: boolean;
|
||||
subTitle?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
onToggle: (value: boolean) => void;
|
||||
toggleDataTestId?: string;
|
||||
}
|
||||
@ -1,12 +1,15 @@
|
||||
import { Axis } from '@visx/axis';
|
||||
import { GridColumns } from '@visx/grid';
|
||||
import { Group } from '@visx/group';
|
||||
import { ParentSize } from '@visx/responsive';
|
||||
import { scaleLinear } from '@visx/scale';
|
||||
import { BoxPlot } from '@visx/stats';
|
||||
import { useTooltip } from '@visx/tooltip';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Margin } from '@visx/xychart';
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import DynamicMarginSetter from '@components/components/BarChart/components/DynamicMarginSetter';
|
||||
import {
|
||||
AXIS_LABEL_MARGIN_OFFSET,
|
||||
AXIS_LABEL_PROPS,
|
||||
@ -20,6 +23,7 @@ import {
|
||||
WhiskerTooltipDatum,
|
||||
} from '@components/components/WhiskerChart/types';
|
||||
import { computeWhiskerOffset } from '@components/components/WhiskerChart/utils';
|
||||
import { abbreviateNumber } from '@components/components/dataviz/utils';
|
||||
|
||||
import { colors } from '@src/alchemy-components/theme';
|
||||
|
||||
@ -29,6 +33,8 @@ const ChartWrapper = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const NUMBER_OF_TICKS = 7;
|
||||
|
||||
function InternalWhiskerChart({
|
||||
data,
|
||||
width,
|
||||
@ -40,16 +46,27 @@ function InternalWhiskerChart({
|
||||
renderTooltip = whiskerChartDefaults.renderTooltip,
|
||||
renderWhisker = whiskerChartDefaults.renderWhisker,
|
||||
}: InternalWhiskerChartProps) {
|
||||
const axisLabelMarginOffset = axisLabel !== undefined ? AXIS_LABEL_MARGIN_OFFSET : 0;
|
||||
const margin = { left: 10, top: 0, right: 10, bottom: 20 + axisLabelMarginOffset };
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const defaultMargin = useMemo(() => {
|
||||
const axisLabelMarginOffset = axisLabel !== undefined ? AXIS_LABEL_MARGIN_OFFSET : 0;
|
||||
return {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 20 + axisLabelMarginOffset,
|
||||
left: 0,
|
||||
};
|
||||
}, [axisLabel]);
|
||||
|
||||
const [dynamicMargin, setDynamicMargin] = useState<Margin>(defaultMargin);
|
||||
|
||||
const finalBoxSize = boxSize ?? DEFAULT_BOX_SIZE;
|
||||
const finalGap = gap ?? DEFAULT_GAP_BETWEEN_WHISKERS;
|
||||
|
||||
const minY = 0;
|
||||
const maxY = height - margin.bottom;
|
||||
const minX = margin.left;
|
||||
const maxX = width - margin.right;
|
||||
const maxY = height - dynamicMargin.bottom;
|
||||
const minX = dynamicMargin.left;
|
||||
const maxX = width - dynamicMargin.right;
|
||||
const chartHeight = maxY - minY;
|
||||
const chartWidth = maxX - minX;
|
||||
|
||||
@ -75,61 +92,74 @@ function InternalWhiskerChart({
|
||||
}, [minX, maxX, minValue, maxValue]);
|
||||
|
||||
return (
|
||||
<svg width={width} height={height}>
|
||||
<GridColumns
|
||||
scale={xScale}
|
||||
x={minX}
|
||||
y1={maxY}
|
||||
y2={minY}
|
||||
width={chartWidth}
|
||||
height={chartHeight}
|
||||
stroke={colors.gray[100]}
|
||||
numTicks={5}
|
||||
/>
|
||||
<div ref={wrapperRef}>
|
||||
<svg width={width} height={height}>
|
||||
<Group className="content-group">
|
||||
<GridColumns
|
||||
scale={xScale}
|
||||
x={minX}
|
||||
y1={maxY}
|
||||
y2={minY}
|
||||
width={chartWidth}
|
||||
height={chartHeight}
|
||||
stroke={colors.gray[100]}
|
||||
numTicks={5}
|
||||
/>
|
||||
|
||||
{dataWithOffsets.map(({ datum, offset }) => (
|
||||
<BoxPlot
|
||||
key={datum.key}
|
||||
horizontal
|
||||
boxWidth={finalBoxSize}
|
||||
min={datum.min}
|
||||
firstQuartile={datum.firstQuartile}
|
||||
median={datum.median}
|
||||
thirdQuartile={datum.thirdQuartile}
|
||||
max={datum.max}
|
||||
valueScale={xScale}
|
||||
top={offset}
|
||||
>
|
||||
{renderWhisker ? (props) => renderWhisker({ datum, tooltip, ...props }) : undefined}
|
||||
</BoxPlot>
|
||||
))}
|
||||
{dataWithOffsets.map(({ datum, offset }) => (
|
||||
<BoxPlot
|
||||
key={datum.key}
|
||||
horizontal
|
||||
boxWidth={finalBoxSize}
|
||||
min={datum.min}
|
||||
firstQuartile={datum.firstQuartile}
|
||||
median={datum.median}
|
||||
thirdQuartile={datum.thirdQuartile}
|
||||
max={datum.max}
|
||||
valueScale={xScale}
|
||||
top={offset}
|
||||
>
|
||||
{renderWhisker ? (props) => renderWhisker({ datum, tooltip, ...props }) : undefined}
|
||||
</BoxPlot>
|
||||
))}
|
||||
|
||||
<line x1={0} x2={width} y1={maxY} y2={maxY} strokeWidth={1} stroke={colors.gray[100]} />
|
||||
<Axis
|
||||
scale={xScale}
|
||||
top={maxY}
|
||||
hideTicks
|
||||
hideAxisLine
|
||||
orientation="bottom"
|
||||
numTicks={7}
|
||||
tickLabelProps={{
|
||||
fontSize: '10px',
|
||||
fontFamily: 'Mulish',
|
||||
fill: colors.gray[1700],
|
||||
}}
|
||||
label={axisLabel}
|
||||
labelProps={AXIS_LABEL_PROPS}
|
||||
/>
|
||||
<line x1={0} x2={width} y1={maxY} y2={maxY} strokeWidth={1} stroke={colors.gray[100]} />
|
||||
</Group>
|
||||
<Axis
|
||||
scale={xScale}
|
||||
top={maxY}
|
||||
hideTicks
|
||||
hideAxisLine
|
||||
orientation="bottom"
|
||||
numTicks={NUMBER_OF_TICKS}
|
||||
tickFormat={abbreviateNumber}
|
||||
tickLabelProps={{
|
||||
fontSize: '10px',
|
||||
fontFamily: 'Mulish',
|
||||
fill: colors.gray[1700],
|
||||
}}
|
||||
label={axisLabel}
|
||||
labelProps={AXIS_LABEL_PROPS}
|
||||
tickClassName="bottom-axis-tick"
|
||||
/>
|
||||
|
||||
{tooltip.tooltipOpen &&
|
||||
renderTooltip?.({
|
||||
x: tooltip.tooltipLeft,
|
||||
y: tooltip.tooltipTop,
|
||||
minY,
|
||||
maxY,
|
||||
datum: tooltip.tooltipData,
|
||||
})}
|
||||
</svg>
|
||||
<DynamicMarginSetter
|
||||
setMargin={setDynamicMargin}
|
||||
wrapperRef={wrapperRef}
|
||||
currentMargin={dynamicMargin}
|
||||
minimalMargin={defaultMargin}
|
||||
/>
|
||||
|
||||
{tooltip.tooltipOpen &&
|
||||
renderTooltip?.({
|
||||
x: tooltip.tooltipLeft,
|
||||
y: tooltip.tooltipTop,
|
||||
minY,
|
||||
maxY,
|
||||
datum: tooltip.tooltipData,
|
||||
})}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -40,6 +40,7 @@ export * from './components/Table';
|
||||
export * from './components/Text';
|
||||
export * from './components/TextArea';
|
||||
export * from './components/Timeline';
|
||||
export * from './components/ToggleCard';
|
||||
export * from './components/Tooltip';
|
||||
export * from './components/Utils';
|
||||
export * from './components/WhiskerChart';
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useDebounce } from 'react-use';
|
||||
|
||||
import { MatchLabelText, SearchContainer, StyledInput } from '@app/entityV2/shared/components/search/styledComponents';
|
||||
import { pluralize } from '@src/app/shared/textUtil';
|
||||
|
||||
interface InlineListSearchProps {
|
||||
searchText: string;
|
||||
debouncedSetFilterText: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
debouncedSetFilterText: (value: string) => void;
|
||||
matchResultCount: number;
|
||||
numRows: number;
|
||||
options?: {
|
||||
@ -25,12 +26,20 @@ export const InlineListSearch: React.FC<InlineListSearchProps> = ({
|
||||
entityTypeName,
|
||||
options,
|
||||
}) => {
|
||||
const [debouncedSearchText, setDebouncedSearchText] = useState(searchText);
|
||||
useDebounce(
|
||||
() => {
|
||||
debouncedSetFilterText(debouncedSearchText);
|
||||
},
|
||||
500,
|
||||
[debouncedSearchText],
|
||||
);
|
||||
return (
|
||||
<SearchContainer>
|
||||
<StyledInput
|
||||
value={searchText}
|
||||
placeholder={options?.placeholder || 'Search...'}
|
||||
onChange={debouncedSetFilterText}
|
||||
onChange={(e) => setDebouncedSearchText(e.target.value)}
|
||||
icon={options?.hidePrefix ? undefined : { icon: 'MagnifyingGlass', source: 'phosphor' }}
|
||||
label=""
|
||||
/>
|
||||
|
||||
@ -63,8 +63,7 @@ export const AcrylAssertionListFilters: React.FC<AcrylAssertionListFiltersProps>
|
||||
}) => {
|
||||
const [appliedRecommendedFilters, setAppliedRecommendedFilters] = useState([]);
|
||||
|
||||
const handleSearchTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const searchText = event.target.value;
|
||||
const handleSearchTextChange = (searchText: string) => {
|
||||
handleFilterChange({
|
||||
...selectedFilters,
|
||||
filterCriteria: { ...selectedFilters.filterCriteria, searchText },
|
||||
|
||||
@ -34,8 +34,7 @@ export const IncidentFilterContainer: React.FC<IncidentAssigneeAvatarStack> = ({
|
||||
handleFilterChange,
|
||||
selectedFilters,
|
||||
}) => {
|
||||
const handleSearchTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const searchText = event.target.value;
|
||||
const handleSearchTextChange = (searchText: string) => {
|
||||
handleFilterChange({
|
||||
...selectedFilters,
|
||||
filterCriteria: { ...selectedFilters.filterCriteria, searchText },
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { FallbackProps, ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
|
||||
|
||||
import ErrorFallback, { ErrorVariant } from '@app/sharedV2/ErrorHandling/ErrorFallback';
|
||||
|
||||
type ErrorBoundaryProps = {
|
||||
variant?: ErrorVariant;
|
||||
children: ReactNode;
|
||||
fallback?: React.ComponentType<FallbackProps>;
|
||||
resetKeys?: string[];
|
||||
};
|
||||
|
||||
const logError = (error: Error, info: { componentStack: string }) => {
|
||||
console.group('🔴 UI Crash Error Report');
|
||||
console.error('Error:', error);
|
||||
console.error('Component Info:', info);
|
||||
console.error('URL:', window.location.href);
|
||||
|
||||
console.warn('🔧 ACTION REQUIRED: Please report this error to your Datahub Administrator');
|
||||
console.warn('📧 Include the above error details in your report');
|
||||
console.groupEnd();
|
||||
};
|
||||
|
||||
export const ErrorBoundary = ({ children, variant = 'route', fallback, resetKeys }: ErrorBoundaryProps) => {
|
||||
const FallbackComponent =
|
||||
fallback ||
|
||||
(() => (
|
||||
<ErrorFallback
|
||||
variant={variant}
|
||||
// Custom message for on-prem customers
|
||||
actionMessage="Please report the error messages from your browser to your Datahub Administrator"
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<ReactErrorBoundary
|
||||
FallbackComponent={FallbackComponent}
|
||||
onError={(e, i) => logError(e, i)}
|
||||
resetKeys={resetKeys}
|
||||
>
|
||||
{children}
|
||||
</ReactErrorBoundary>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,94 @@
|
||||
import { Button, Text } from '@components';
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
type ErrorFallbackProps = {
|
||||
variant?: ErrorVariant;
|
||||
actionMessage?: string;
|
||||
};
|
||||
const DEFAULT_MESSAGE = 'Our team has been notified of this unexpected error and is working on a resolution.';
|
||||
export type ErrorVariant = 'route' | 'tab' | 'sidebar';
|
||||
|
||||
const getVariantStyles = (variant: ErrorVariant) => {
|
||||
// only route needs the border shadows and margins
|
||||
if (variant === 'route') {
|
||||
return css`
|
||||
border-radius: ${(props) => props.theme.styles['border-radius-navbar-redesign']};
|
||||
box-shadow: ${(props) => props.theme.styles['box-shadow-navbar-redesign']};
|
||||
margin: 5px;
|
||||
`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const Container = styled.div<{ variant?: ErrorVariant }>`
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
gap: 16px;
|
||||
|
||||
${(props) => getVariantStyles(props.variant || 'route')}
|
||||
|
||||
svg {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ButtonContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
const TextContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const ErrorFallback: React.FC<ErrorFallbackProps> = ({ variant, actionMessage = DEFAULT_MESSAGE }) => {
|
||||
const history = useHistory();
|
||||
return (
|
||||
<Container variant={variant}>
|
||||
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="200" height="200" rx="100" fill="#F9FAFC" />
|
||||
<path
|
||||
d="M110 145C110 146.978 109.414 148.911 108.315 150.556C107.216 152.2 105.654 153.482 103.827 154.239C102 154.996 99.9889 155.194 98.0491 154.808C96.1093 154.422 94.3275 153.47 92.9289 152.071C91.5304 150.673 90.578 148.891 90.1922 146.951C89.8063 145.011 90.0043 143 90.7612 141.173C91.5181 139.346 92.7998 137.784 94.4443 136.685C96.0888 135.587 98.0222 135 100 135C102.652 135 105.196 136.054 107.071 137.929C108.946 139.804 110 142.348 110 145ZM100 120C101.326 120 102.598 119.473 103.536 118.536C104.473 117.598 105 116.326 105 115V50C105 48.6739 104.473 47.4021 103.536 46.4645C102.598 45.5268 101.326 45 100 45C98.6739 45 97.4022 45.5268 96.4645 46.4645C95.5268 47.4021 95 48.6739 95 50V115C95 116.326 95.5268 117.598 96.4645 118.536C97.4022 119.473 98.6739 120 100 120Z"
|
||||
fill="#67739E"
|
||||
/>
|
||||
</svg>
|
||||
<TextContainer>
|
||||
<Text color="gray" weight="bold" size="xl">
|
||||
Whoops!
|
||||
</Text>
|
||||
<TextContainer>
|
||||
<Text color="gray" size="lg">
|
||||
Something didn't go as planned.
|
||||
</Text>
|
||||
<Text color="gray" size="lg">
|
||||
{actionMessage}
|
||||
</Text>
|
||||
</TextContainer>
|
||||
</TextContainer>
|
||||
{variant !== 'sidebar' && (
|
||||
<ButtonContainer>
|
||||
<Button size="sm" variant="outline" onClick={() => history.go(0)}>
|
||||
Refresh
|
||||
</Button>
|
||||
<Button size="sm" variant="filled" onClick={() => history.push('/')}>
|
||||
Home
|
||||
</Button>
|
||||
</ButtonContainer>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorFallback;
|
||||
Loading…
x
Reference in New Issue
Block a user