mirror of
https://github.com/datahub-project/datahub.git
synced 2025-09-25 09:00:50 +00:00
fix(ui): Various component library updates (#14249)
Co-authored-by: John Joyce <john@Mac-4089.lan> Co-authored-by: John Joyce <john@Mac-4260.lan>
This commit is contained in:
parent
c5a8d04496
commit
6e8c8c6b85
@ -16,6 +16,7 @@ export function DatePicker({
|
|||||||
variant = datePickerDefault.variant,
|
variant = datePickerDefault.variant,
|
||||||
disabled = datePickerDefault.disabled,
|
disabled = datePickerDefault.disabled,
|
||||||
disabledDate,
|
disabledDate,
|
||||||
|
placeholder,
|
||||||
}: DatePickerProps) {
|
}: DatePickerProps) {
|
||||||
const [internalValue, setInternalValue] = useState<DatePickerValue | undefined>(value);
|
const [internalValue, setInternalValue] = useState<DatePickerValue | undefined>(value);
|
||||||
|
|
||||||
@ -38,8 +39,9 @@ export function DatePicker({
|
|||||||
open: isOpen,
|
open: isOpen,
|
||||||
setValue: setInternalValue,
|
setValue: setInternalValue,
|
||||||
},
|
},
|
||||||
|
placeholder,
|
||||||
});
|
});
|
||||||
}, [disabled, isOpen, inputRender]);
|
}, [disabled, placeholder, isOpen, inputRender]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledAntdDatePicker
|
<StyledAntdDatePicker
|
||||||
|
@ -9,6 +9,7 @@ export type DatePickerProps = {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
disabledDate?: (value: DatePickerValue) => boolean;
|
disabledDate?: (value: DatePickerValue) => boolean;
|
||||||
variant?: DatePickerVariant;
|
variant?: DatePickerVariant;
|
||||||
|
placeholder?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DatePickerState = {
|
export type DatePickerState = {
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
import { Input } from '@components';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { ExtendedInputRenderProps } from '@components/components/DatePicker/types';
|
||||||
|
|
||||||
|
export function DefaultDatePickerInput({ datePickerProps, ...props }: ExtendedInputRenderProps) {
|
||||||
|
const { disabled } = datePickerProps;
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
{...props}
|
||||||
|
label=""
|
||||||
|
value={props.value || ''}
|
||||||
|
isDisabled={disabled}
|
||||||
|
placeholder={props.placeholder || 'Select date'}
|
||||||
|
icon={{ icon: 'CalendarMonth' }}
|
||||||
|
isReadOnly
|
||||||
|
style={{
|
||||||
|
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||||
|
...props.style,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -2,7 +2,14 @@ import React from 'react';
|
|||||||
|
|
||||||
import { StyledCalendarWrapper } from '@components/components/DatePicker/components';
|
import { StyledCalendarWrapper } from '@components/components/DatePicker/components';
|
||||||
import { VariantProps } from '@components/components/DatePicker/types';
|
import { VariantProps } from '@components/components/DatePicker/types';
|
||||||
|
import { DefaultDatePickerInput } from '@components/components/DatePicker/variants/common/components';
|
||||||
|
|
||||||
export const CommonVariantProps: VariantProps = {
|
export const CommonVariantProps: VariantProps = {
|
||||||
panelRender: (panel) => <StyledCalendarWrapper>{panel}</StyledCalendarWrapper>,
|
panelRender: (panel) => <StyledCalendarWrapper>{panel}</StyledCalendarWrapper>,
|
||||||
|
inputRender: (props) => <DefaultDatePickerInput {...props} />,
|
||||||
|
bordered: false,
|
||||||
|
allowClear: false,
|
||||||
|
format: 'll',
|
||||||
|
suffixIcon: null,
|
||||||
|
$noDefaultPaddings: true,
|
||||||
};
|
};
|
||||||
|
@ -49,7 +49,7 @@ const CaretWrapper = styled.div<{ $disabled?: boolean }>`
|
|||||||
& svg {
|
& svg {
|
||||||
color: ${colors.gray[1800]};
|
color: ${colors.gray[1800]};
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: start;
|
||||||
cursor: ${(props) => (props.$disabled ? 'not-allowed' : 'pointer')};
|
cursor: ${(props) => (props.$disabled ? 'not-allowed' : 'pointer')};
|
||||||
|
|
||||||
:hover {
|
:hover {
|
||||||
|
@ -6,10 +6,5 @@ import { DateSwitcherInput } from '@components/components/DatePicker/variants/da
|
|||||||
|
|
||||||
export const DateSwitcherVariantProps: VariantProps = {
|
export const DateSwitcherVariantProps: VariantProps = {
|
||||||
...CommonVariantProps,
|
...CommonVariantProps,
|
||||||
bordered: false,
|
|
||||||
allowClear: false,
|
|
||||||
format: 'll',
|
|
||||||
suffixIcon: null,
|
|
||||||
inputRender: (props) => <DateSwitcherInput {...props} />,
|
inputRender: (props) => <DateSwitcherInput {...props} />,
|
||||||
$noDefaultPaddings: true,
|
|
||||||
};
|
};
|
||||||
|
@ -31,7 +31,7 @@ export const InputWrapper = styled.div({
|
|||||||
export const InputContainer = styled.div(
|
export const InputContainer = styled.div(
|
||||||
({ isSuccess, warning, isDisabled, isInvalid }: InputProps) => ({
|
({ isSuccess, warning, isDisabled, isInvalid }: InputProps) => ({
|
||||||
border: `${borders['1px']} ${getStatusColors(isSuccess, warning, isInvalid)}`,
|
border: `${borders['1px']} ${getStatusColors(isSuccess, warning, isInvalid)}`,
|
||||||
backgroundColor: isDisabled ? colors.gray[100] : colors.white,
|
backgroundColor: isDisabled ? colors.gray[1500] : colors.white,
|
||||||
paddingRight: spacing.md,
|
paddingRight: spacing.md,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
@ -67,6 +67,10 @@ export const InputField = styled.input({
|
|||||||
'&:focus': {
|
'&:focus': {
|
||||||
outline: 'none',
|
outline: 'none',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'&:disabled': {
|
||||||
|
backgroundColor: colors.gray[1500],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Required = styled.span({
|
export const Required = styled.span({
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
import { Button, Icon } from '@components';
|
import { Button } from '@components';
|
||||||
import { Checkbox } from 'antd';
|
import { Checkbox } from 'antd';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { Icon } from '@components/components/Icon';
|
||||||
import { SelectLabelVariants, SelectSizeOptions, SelectStyleProps } from '@components/components/Select/types';
|
import { SelectLabelVariants, SelectSizeOptions, SelectStyleProps } from '@components/components/Select/types';
|
||||||
import { getOptionLabelStyle, getSelectFontStyles, getSelectStyle } from '@components/components/Select/utils';
|
import {
|
||||||
|
getDropdownStyle,
|
||||||
|
getOptionLabelStyle,
|
||||||
|
getSelectFontStyles,
|
||||||
|
getSelectStyle,
|
||||||
|
} from '@components/components/Select/utils';
|
||||||
import {
|
import {
|
||||||
formLabelTextStyles,
|
formLabelTextStyles,
|
||||||
inputPlaceholderTextStyles,
|
inputPlaceholderTextStyles,
|
||||||
@ -40,7 +46,7 @@ export const SelectLabelContainer = styled.div({
|
|||||||
gap: spacing.xsm,
|
gap: spacing.xsm,
|
||||||
lineHeight: typography.lineHeights.none,
|
lineHeight: typography.lineHeights.none,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
maxWidth: 'calc(100% - 54px)',
|
maxWidth: 'calc(100% - 10px)',
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -89,6 +95,7 @@ export const Container = styled.div<ContainerProps>(({ size, width, $selectLabel
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const DropdownContainer = styled.div<{ ignoreMaxHeight?: boolean }>(({ ignoreMaxHeight }) => ({
|
export const DropdownContainer = styled.div<{ ignoreMaxHeight?: boolean }>(({ ignoreMaxHeight }) => ({
|
||||||
|
...getDropdownStyle(),
|
||||||
borderRadius: radius.md,
|
borderRadius: radius.md,
|
||||||
background: colors.white,
|
background: colors.white,
|
||||||
zIndex: zIndices.dropdown,
|
zIndex: zIndices.dropdown,
|
||||||
@ -220,8 +227,8 @@ export const ArrowIcon = styled.span<{ isOpen: boolean }>(({ isOpen }) => ({
|
|||||||
|
|
||||||
export const StyledCheckbox = styled(Checkbox)({
|
export const StyledCheckbox = styled(Checkbox)({
|
||||||
'.ant-checkbox-checked:not(.ant-checkbox-disabled) .ant-checkbox-inner': {
|
'.ant-checkbox-checked:not(.ant-checkbox-disabled) .ant-checkbox-inner': {
|
||||||
backgroundColor: colors.violet[500],
|
backgroundColor: `${(props) => props.theme.styles['primary-color']}`,
|
||||||
borderColor: `${colors.violet[500]} !important`,
|
borderColor: `${(props) => props.theme.styles['primary-color']} !important`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -109,11 +109,11 @@ export const getSelectStyle = (props: SelectStyleProps) => {
|
|||||||
|
|
||||||
const baseStyle = {
|
const baseStyle = {
|
||||||
borderRadius: radius.md,
|
borderRadius: radius.md,
|
||||||
border: `1px solid ${isDisabled ? colors.gray[1800] : colors.gray[100]}`,
|
border: `1px solid ${colors.gray[100]}`,
|
||||||
fontFamily: typography.fonts.body,
|
fontFamily: typography.fonts.body,
|
||||||
|
backgroundColor: isDisabled ? colors.gray[1500] : colors.white,
|
||||||
color: isDisabled ? colors.gray[300] : colors.gray[600],
|
color: isDisabled ? colors.gray[300] : colors.gray[600],
|
||||||
cursor: isDisabled || isReadOnly ? 'not-allowed' : 'pointer',
|
cursor: isDisabled || isReadOnly ? 'not-allowed' : 'pointer',
|
||||||
backgroundColor: isDisabled ? colors.gray[1500] : 'initial',
|
|
||||||
boxShadow: '0px 1px 2px 0px rgba(33, 23, 95, 0.07)',
|
boxShadow: '0px 1px 2px 0px rgba(33, 23, 95, 0.07)',
|
||||||
textWrap: 'nowrap',
|
textWrap: 'nowrap',
|
||||||
|
|
||||||
@ -125,7 +125,7 @@ export const getSelectStyle = (props: SelectStyleProps) => {
|
|||||||
...(isOpen
|
...(isOpen
|
||||||
? {
|
? {
|
||||||
borderColor: colors.gray[1800],
|
borderColor: colors.gray[1800],
|
||||||
outline: `1px solid ${colors.violet[300]}`,
|
outline: `1px solid ${colors.violet[200]}`,
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
|
|
||||||
@ -150,3 +150,11 @@ export const getSelectStyle = (props: SelectStyleProps) => {
|
|||||||
...minHeightStyles,
|
...minHeightStyles,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDropdownStyle = () => {
|
||||||
|
const baseStyle = {
|
||||||
|
fontFamily: typography.fonts.body,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...baseStyle };
|
||||||
|
};
|
||||||
|
@ -38,7 +38,7 @@ export const StyledIcon = styled(Icon)({
|
|||||||
export const TextAreaContainer = styled.div(
|
export const TextAreaContainer = styled.div(
|
||||||
({ isSuccess, warning, isDisabled, isInvalid }: TextAreaProps) => ({
|
({ isSuccess, warning, isDisabled, isInvalid }: TextAreaProps) => ({
|
||||||
border: `${borders['1px']} ${getStatusColors(isSuccess, warning, isInvalid)}`,
|
border: `${borders['1px']} ${getStatusColors(isSuccess, warning, isInvalid)}`,
|
||||||
backgroundColor: isDisabled ? colors.gray[100] : colors.white,
|
backgroundColor: isDisabled ? colors.gray[1500] : colors.white,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
...defaultFlexStyles,
|
...defaultFlexStyles,
|
||||||
@ -78,6 +78,10 @@ export const TextAreaField = styled.textarea<{ icon?: IconNames }>(({ icon }) =>
|
|||||||
'&::placeholder': {
|
'&::placeholder': {
|
||||||
...inputPlaceholderTextStyles,
|
...inputPlaceholderTextStyles,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'&:disabled': {
|
||||||
|
backgroundColor: colors.gray[1500],
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const Label = styled.div({
|
export const Label = styled.div({
|
||||||
|
@ -11,6 +11,7 @@ export * from './components/Button';
|
|||||||
export * from './components/CalendarChart';
|
export * from './components/CalendarChart';
|
||||||
export * from './components/Card';
|
export * from './components/Card';
|
||||||
export * from './components/Checkbox';
|
export * from './components/Checkbox';
|
||||||
|
export * from './components/ColorPicker';
|
||||||
export * from './components/DatePicker';
|
export * from './components/DatePicker';
|
||||||
export * from './components/Drawer';
|
export * from './components/Drawer';
|
||||||
export * from './components/Dropdown';
|
export * from './components/Dropdown';
|
||||||
@ -29,6 +30,7 @@ export * from './components/Pills';
|
|||||||
export * from './components/Popover';
|
export * from './components/Popover';
|
||||||
export * from './components/SearchBar';
|
export * from './components/SearchBar';
|
||||||
export * from './components/Select';
|
export * from './components/Select';
|
||||||
|
export * from './components/StructuredPopover';
|
||||||
export * from './components/Switch';
|
export * from './components/Switch';
|
||||||
export * from './components/Tabs';
|
export * from './components/Tabs';
|
||||||
export * from './components/Table';
|
export * from './components/Table';
|
||||||
@ -38,5 +40,3 @@ export * from './components/Timeline';
|
|||||||
export * from './components/Tooltip';
|
export * from './components/Tooltip';
|
||||||
export * from './components/Utils';
|
export * from './components/Utils';
|
||||||
export * from './components/WhiskerChart';
|
export * from './components/WhiskerChart';
|
||||||
export * from './components/ColorPicker';
|
|
||||||
export * from './components/StructuredPopover';
|
|
||||||
|
@ -5,7 +5,6 @@ import { Theme } from '@conf/theme/types';
|
|||||||
|
|
||||||
import { ColorOptions, DEFAULT_VALUE, FontSizeOptions, MiscColorOptions, RotationOptions } from './config';
|
import { ColorOptions, DEFAULT_VALUE, FontSizeOptions, MiscColorOptions, RotationOptions } from './config';
|
||||||
import { foundations } from './foundations';
|
import { foundations } from './foundations';
|
||||||
import { semanticTokens } from './semantic-tokens';
|
|
||||||
|
|
||||||
const { colors, typography, transform } = foundations;
|
const { colors, typography, transform } = foundations;
|
||||||
/*
|
/*
|
||||||
@ -67,5 +66,5 @@ export const getStatusColors = (isSuccess?: boolean, warning?: string, isInvalid
|
|||||||
if (warning) {
|
if (warning) {
|
||||||
return colors.yellow[600];
|
return colors.yellow[600];
|
||||||
}
|
}
|
||||||
return semanticTokens.colors['border-color'];
|
return colors.gray[100];
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,7 @@ import { Tooltip } from '@components';
|
|||||||
import { Select, Tag } from 'antd';
|
import { Select, Tag } from 'antd';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { EntitySearchInputResult } from '@app/entityV2/shared/EntitySearchInput/EntitySearchInputResult';
|
import EntitySearchInputResultV2 from '@app/entityV2/shared/EntitySearchInput/EntitySearchInputResultV2';
|
||||||
import { useEntityRegistry } from '@app/useEntityRegistry';
|
import { useEntityRegistry } from '@app/useEntityRegistry';
|
||||||
|
|
||||||
import { useGetEntitiesLazyQuery } from '@graphql/entity.generated';
|
import { useGetEntitiesLazyQuery } from '@graphql/entity.generated';
|
||||||
@ -173,7 +173,7 @@ export const EntitySearchInput = ({
|
|||||||
style={optionStyle}
|
style={optionStyle}
|
||||||
data-testid={`${result.entity.urn}-entity-search-input-result`}
|
data-testid={`${result.entity.urn}-entity-search-input-result`}
|
||||||
>
|
>
|
||||||
<EntitySearchInputResult entity={result.entity} />
|
<EntitySearchInputResultV2 entity={result.entity} />
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { Icon, Text } from '@components';
|
import { Text } from '@components';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import EntityRegistry from '@app/entityV2/EntityRegistry';
|
||||||
import { getDisplayedEntityType } from '@app/entityV2/shared/containers/profile/header/utils';
|
import { getDisplayedEntityType } from '@app/entityV2/shared/containers/profile/header/utils';
|
||||||
import ContextPath from '@app/previewV2/ContextPath';
|
import ContextPath from '@app/previewV2/ContextPath';
|
||||||
import { useEntityRegistry } from '@app/useEntityRegistry';
|
import { useEntityRegistry } from '@app/useEntityRegistry';
|
||||||
@ -17,6 +18,7 @@ const Wrapper = styled.div`
|
|||||||
const TextWrapper = styled.div`
|
const TextWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
// TODO: Add this as a prop if needed
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -30,7 +32,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function EntitySearchInputResultV2({ entity }: Props) {
|
export default function EntitySearchInputResultV2({ entity }: Props) {
|
||||||
const entityRegistry = useEntityRegistry();
|
const entityRegistry = useEntityRegistry() as EntityRegistry;
|
||||||
const properties = entityRegistry.getGenericEntityProperties(entity.type, entity);
|
const properties = entityRegistry.getGenericEntityProperties(entity.type, entity);
|
||||||
const platformIcon = properties?.platform?.properties?.logoUrl;
|
const platformIcon = properties?.platform?.properties?.logoUrl;
|
||||||
|
|
||||||
@ -38,10 +40,9 @@ export default function EntitySearchInputResultV2({ entity }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
{!platformIcon && <Icon size="4xl" source="phosphor" icon="Placeholder" />}
|
|
||||||
{platformIcon && <IconContainer src={platformIcon} />}
|
{platformIcon && <IconContainer src={platformIcon} />}
|
||||||
<TextWrapper>
|
<TextWrapper>
|
||||||
<Text size="lg">{entityRegistry.getDisplayName(entity.type, entity)}</Text>
|
<Text size="md">{entityRegistry.getDisplayName(entity.type, entity)}</Text>
|
||||||
<ContextPath
|
<ContextPath
|
||||||
entityType={entity.type}
|
entityType={entity.type}
|
||||||
displayedEntityType={displayedEntityType}
|
displayedEntityType={displayedEntityType}
|
||||||
|
@ -0,0 +1,342 @@
|
|||||||
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import Dropdown from '@components/components/Dropdown/Dropdown';
|
||||||
|
import { Input } from '@components/components/Input/Input';
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
DropdownContainer,
|
||||||
|
LabelContainer,
|
||||||
|
OptionContainer,
|
||||||
|
OptionLabel,
|
||||||
|
OptionList,
|
||||||
|
SelectBase,
|
||||||
|
SelectLabel,
|
||||||
|
SelectLabelContainer,
|
||||||
|
StyledCheckbox,
|
||||||
|
StyledIcon,
|
||||||
|
} from '@components/components/Select/components';
|
||||||
|
import SelectActionButtons from '@components/components/Select/private/SelectActionButtons';
|
||||||
|
import SelectLabelRenderer from '@components/components/Select/private/SelectLabelRenderer/SelectLabelRenderer';
|
||||||
|
import useSelectDropdown from '@components/components/Select/private/hooks/useSelectDropdown';
|
||||||
|
import { SelectOption, SelectSizeOptions } from '@components/components/Select/types';
|
||||||
|
|
||||||
|
import EntitySearchInputResultV2 from '@app/entityV2/shared/EntitySearchInput/EntitySearchInputResultV2';
|
||||||
|
import { useEntityRegistry } from '@app/useEntityRegistry';
|
||||||
|
|
||||||
|
import { useGetEntitiesLazyQuery } from '@graphql/entity.generated';
|
||||||
|
import { useGetSearchResultsForMultipleLazyQuery } from '@graphql/search.generated';
|
||||||
|
import { Entity, EntityType } from '@types';
|
||||||
|
|
||||||
|
const EmptyState = styled.div`
|
||||||
|
padding: 16px 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: #8c8c8c;
|
||||||
|
font-style: italic;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const LoadingState = styled.div`
|
||||||
|
padding: 16px 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: #8c8c8c;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SearchInputContainer = styled.div`
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const EntityOptionContainer = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export interface EntitySearchSelectProps {
|
||||||
|
selectedUrns?: string[];
|
||||||
|
entityTypes: EntityType[];
|
||||||
|
placeholder?: string;
|
||||||
|
size?: SelectSizeOptions;
|
||||||
|
isMultiSelect?: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
isReadOnly?: boolean;
|
||||||
|
label?: string;
|
||||||
|
width?: number | 'full' | 'fit-content';
|
||||||
|
onUpdate?: (selectedUrns: string[]) => void;
|
||||||
|
showClear?: boolean;
|
||||||
|
isRequired?: boolean;
|
||||||
|
icon?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EntityOption extends SelectOption {
|
||||||
|
entity: Entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addToCache = (cache: Map<string, Entity>, entity: Entity) => {
|
||||||
|
cache.set(entity.urn, entity);
|
||||||
|
return cache;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildCache = (entities: Entity[]) => {
|
||||||
|
const cache = new Map();
|
||||||
|
entities.forEach((entity) => cache.set(entity.urn, entity));
|
||||||
|
return cache;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isResolutionRequired = (urns: string[], cache: Map<string, Entity>) => {
|
||||||
|
const uncachedUrns = urns.filter((urn) => !cache.has(urn));
|
||||||
|
return uncachedUrns.length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A standardized entity search and selection component that allows users to search
|
||||||
|
* for DataHub entities and select one or multiple entities. Built on top of the
|
||||||
|
* Select component library infrastructure for consistency.
|
||||||
|
*/
|
||||||
|
export const EntitySearchSelect: React.FC<EntitySearchSelectProps> = ({
|
||||||
|
selectedUrns = [],
|
||||||
|
entityTypes,
|
||||||
|
placeholder = 'Search for entities...',
|
||||||
|
size = 'md',
|
||||||
|
isMultiSelect = false,
|
||||||
|
isDisabled = false,
|
||||||
|
isReadOnly = false,
|
||||||
|
label,
|
||||||
|
width = 255,
|
||||||
|
onUpdate,
|
||||||
|
showClear = true,
|
||||||
|
isRequired = false,
|
||||||
|
icon,
|
||||||
|
}) => {
|
||||||
|
const entityRegistry = useEntityRegistry();
|
||||||
|
const [entityCache, setEntityCache] = useState<Map<string, Entity>>(new Map());
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const selectRef = useRef<HTMLDivElement>(null);
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
isVisible,
|
||||||
|
close: closeDropdown,
|
||||||
|
toggle: toggleDropdown,
|
||||||
|
} = useSelectDropdown(false, selectRef, dropdownRef);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap by resolving all URNs that are not in the cache yet.
|
||||||
|
*/
|
||||||
|
const [getEntities, { data: resolvedEntitiesData, loading: entitiesLoading }] = useGetEntitiesLazyQuery();
|
||||||
|
useEffect(() => {
|
||||||
|
if (isResolutionRequired(selectedUrns, entityCache)) {
|
||||||
|
getEntities({ variables: { urns: selectedUrns } });
|
||||||
|
}
|
||||||
|
}, [selectedUrns, entityCache, getEntities]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build cache from resolved entities
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (resolvedEntitiesData && resolvedEntitiesData.entities?.length) {
|
||||||
|
const entities: Entity[] = (resolvedEntitiesData?.entities as Entity[]) || [];
|
||||||
|
setEntityCache(buildCache(entities));
|
||||||
|
}
|
||||||
|
}, [resolvedEntitiesData]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search functionality
|
||||||
|
*/
|
||||||
|
const [searchResources, { data: resourcesSearchData, loading: searchLoading }] =
|
||||||
|
useGetSearchResultsForMultipleLazyQuery();
|
||||||
|
|
||||||
|
const entityOptions: EntityOption[] = useMemo(() => {
|
||||||
|
const results = resourcesSearchData?.searchAcrossEntities?.searchResults || [];
|
||||||
|
return results.map((result) => ({
|
||||||
|
label: entityRegistry.getDisplayName(result.entity.type, result.entity),
|
||||||
|
value: result.entity.urn,
|
||||||
|
entity: result.entity as Entity,
|
||||||
|
}));
|
||||||
|
}, [resourcesSearchData, entityRegistry]);
|
||||||
|
|
||||||
|
const handleSelectClick = useCallback(() => {
|
||||||
|
if (!isDisabled && !isReadOnly) {
|
||||||
|
toggleDropdown();
|
||||||
|
}
|
||||||
|
}, [toggleDropdown, isDisabled, isReadOnly]);
|
||||||
|
|
||||||
|
const handleOptionClick = useCallback(
|
||||||
|
(option: EntityOption) => {
|
||||||
|
// Add entity to cache
|
||||||
|
setEntityCache(addToCache(entityCache, option.entity));
|
||||||
|
|
||||||
|
let newUrns: string[];
|
||||||
|
if (isMultiSelect) {
|
||||||
|
newUrns = selectedUrns.includes(option.value)
|
||||||
|
? selectedUrns.filter((urn) => urn !== option.value)
|
||||||
|
: [...selectedUrns, option.value];
|
||||||
|
} else {
|
||||||
|
newUrns = [option.value];
|
||||||
|
closeDropdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdate?.(newUrns);
|
||||||
|
},
|
||||||
|
[selectedUrns, isMultiSelect, onUpdate, closeDropdown, entityCache],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSearchChange = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
setSearchQuery(value);
|
||||||
|
searchResources({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
types: entityTypes,
|
||||||
|
query: value || '*',
|
||||||
|
start: 0,
|
||||||
|
count: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[entityTypes, searchResources],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClearSelection = useCallback(() => {
|
||||||
|
onUpdate?.([]);
|
||||||
|
}, [onUpdate]);
|
||||||
|
|
||||||
|
const removeOption = useCallback(
|
||||||
|
(option: SelectOption) => {
|
||||||
|
const newUrns = selectedUrns.filter((urn) => urn !== option.value);
|
||||||
|
onUpdate?.(newUrns);
|
||||||
|
},
|
||||||
|
[selectedUrns, onUpdate],
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Issue a default search on component mount
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
searchResources({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
types: entityTypes,
|
||||||
|
query: '*',
|
||||||
|
start: 0,
|
||||||
|
count: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [entityTypes, searchResources]);
|
||||||
|
|
||||||
|
// Create options for selected values from cache
|
||||||
|
const selectedOptions: SelectOption[] = useMemo(() => {
|
||||||
|
return selectedUrns.map((urn) => {
|
||||||
|
const entity = entityCache.get(urn);
|
||||||
|
return {
|
||||||
|
label: entity ? entityRegistry.getDisplayName(entity.type, entity) : urn,
|
||||||
|
value: urn,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [selectedUrns, entityCache, entityRegistry]);
|
||||||
|
|
||||||
|
const isLoading = entitiesLoading || searchLoading;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container ref={selectRef} size={size} width={width}>
|
||||||
|
{label && <SelectLabel onClick={handleSelectClick}>{label}</SelectLabel>}
|
||||||
|
{isVisible && (
|
||||||
|
<Dropdown
|
||||||
|
open={isOpen}
|
||||||
|
disabled={isDisabled}
|
||||||
|
placement="bottomRight"
|
||||||
|
dropdownRender={() => (
|
||||||
|
<DropdownContainer ref={dropdownRef}>
|
||||||
|
<SearchInputContainer>
|
||||||
|
<Input
|
||||||
|
label=""
|
||||||
|
value={searchQuery}
|
||||||
|
setValue={(value) => {
|
||||||
|
const newValue = typeof value === 'function' ? value(searchQuery) : value;
|
||||||
|
handleSearchChange(newValue);
|
||||||
|
}}
|
||||||
|
placeholder="Search..."
|
||||||
|
icon={{ icon: 'Search' }}
|
||||||
|
data-testid="entity-search-select-input"
|
||||||
|
/>
|
||||||
|
</SearchInputContainer>
|
||||||
|
<OptionList>
|
||||||
|
{isLoading && (
|
||||||
|
<LoadingState>
|
||||||
|
<LoadingOutlined />
|
||||||
|
</LoadingState>
|
||||||
|
)}
|
||||||
|
{!isLoading && entityOptions.length === 0 && <EmptyState>No entities found</EmptyState>}
|
||||||
|
{!isLoading &&
|
||||||
|
entityOptions.map((option) => (
|
||||||
|
<OptionLabel
|
||||||
|
key={option.value}
|
||||||
|
onClick={() => !isMultiSelect && handleOptionClick(option)}
|
||||||
|
isSelected={selectedUrns.includes(option.value)}
|
||||||
|
isMultiSelect={isMultiSelect}
|
||||||
|
data-testid={`entity-search-option-${option.entity.urn.split(':').pop()}`}
|
||||||
|
>
|
||||||
|
{isMultiSelect ? (
|
||||||
|
<LabelContainer>
|
||||||
|
<EntityOptionContainer>
|
||||||
|
<EntitySearchInputResultV2 entity={option.entity} />
|
||||||
|
</EntityOptionContainer>
|
||||||
|
<StyledCheckbox
|
||||||
|
onClick={() => handleOptionClick(option)}
|
||||||
|
checked={selectedUrns.includes(option.value)}
|
||||||
|
/>
|
||||||
|
</LabelContainer>
|
||||||
|
) : (
|
||||||
|
<OptionContainer>
|
||||||
|
<EntitySearchInputResultV2 entity={option.entity} />
|
||||||
|
</OptionContainer>
|
||||||
|
)}
|
||||||
|
</OptionLabel>
|
||||||
|
))}
|
||||||
|
</OptionList>
|
||||||
|
</DropdownContainer>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SelectBase
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
isReadOnly={isReadOnly}
|
||||||
|
isRequired={isRequired}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClick={handleSelectClick}
|
||||||
|
fontSize={size}
|
||||||
|
width={width}
|
||||||
|
data-testid="entity-search-select-select"
|
||||||
|
>
|
||||||
|
<SelectLabelContainer>
|
||||||
|
{icon && <StyledIcon icon={icon} size="lg" />}
|
||||||
|
<SelectLabelRenderer
|
||||||
|
selectedValues={selectedUrns}
|
||||||
|
options={selectedOptions}
|
||||||
|
placeholder={placeholder}
|
||||||
|
isMultiSelect={isMultiSelect}
|
||||||
|
removeOption={removeOption}
|
||||||
|
disabledValues={[]}
|
||||||
|
showDescriptions={false}
|
||||||
|
/>
|
||||||
|
</SelectLabelContainer>
|
||||||
|
<SelectActionButtons
|
||||||
|
hasSelectedValues={selectedUrns.length > 0}
|
||||||
|
isOpen={isOpen}
|
||||||
|
isDisabled={!!isDisabled}
|
||||||
|
isReadOnly={!!isReadOnly}
|
||||||
|
handleClearSelection={handleClearSelection}
|
||||||
|
fontSize={size}
|
||||||
|
showClear={!!showClear}
|
||||||
|
/>
|
||||||
|
</SelectBase>
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user