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:
John Joyce 2025-07-29 11:28:36 -07:00 committed by GitHub
parent c5a8d04496
commit 6e8c8c6b85
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 420 additions and 27 deletions

View File

@ -16,6 +16,7 @@ export function DatePicker({
variant = datePickerDefault.variant,
disabled = datePickerDefault.disabled,
disabledDate,
placeholder,
}: DatePickerProps) {
const [internalValue, setInternalValue] = useState<DatePickerValue | undefined>(value);
@ -38,8 +39,9 @@ export function DatePicker({
open: isOpen,
setValue: setInternalValue,
},
placeholder,
});
}, [disabled, isOpen, inputRender]);
}, [disabled, placeholder, isOpen, inputRender]);
return (
<StyledAntdDatePicker

View File

@ -9,6 +9,7 @@ export type DatePickerProps = {
disabled?: boolean;
disabledDate?: (value: DatePickerValue) => boolean;
variant?: DatePickerVariant;
placeholder?: string;
};
export type DatePickerState = {

View File

@ -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,
}}
/>
);
}

View File

@ -2,7 +2,14 @@ import React from 'react';
import { StyledCalendarWrapper } from '@components/components/DatePicker/components';
import { VariantProps } from '@components/components/DatePicker/types';
import { DefaultDatePickerInput } from '@components/components/DatePicker/variants/common/components';
export const CommonVariantProps: VariantProps = {
panelRender: (panel) => <StyledCalendarWrapper>{panel}</StyledCalendarWrapper>,
inputRender: (props) => <DefaultDatePickerInput {...props} />,
bordered: false,
allowClear: false,
format: 'll',
suffixIcon: null,
$noDefaultPaddings: true,
};

View File

@ -49,7 +49,7 @@ const CaretWrapper = styled.div<{ $disabled?: boolean }>`
& svg {
color: ${colors.gray[1800]};
display: flex;
align-items: center;
align-items: start;
cursor: ${(props) => (props.$disabled ? 'not-allowed' : 'pointer')};
:hover {

View File

@ -6,10 +6,5 @@ import { DateSwitcherInput } from '@components/components/DatePicker/variants/da
export const DateSwitcherVariantProps: VariantProps = {
...CommonVariantProps,
bordered: false,
allowClear: false,
format: 'll',
suffixIcon: null,
inputRender: (props) => <DateSwitcherInput {...props} />,
$noDefaultPaddings: true,
};

View File

@ -31,7 +31,7 @@ export const InputWrapper = styled.div({
export const InputContainer = styled.div(
({ isSuccess, warning, isDisabled, isInvalid }: InputProps) => ({
border: `${borders['1px']} ${getStatusColors(isSuccess, warning, isInvalid)}`,
backgroundColor: isDisabled ? colors.gray[100] : colors.white,
backgroundColor: isDisabled ? colors.gray[1500] : colors.white,
paddingRight: spacing.md,
}),
{
@ -67,6 +67,10 @@ export const InputField = styled.input({
'&:focus': {
outline: 'none',
},
'&:disabled': {
backgroundColor: colors.gray[1500],
},
});
export const Required = styled.span({

View File

@ -1,9 +1,15 @@
import { Button, Icon } from '@components';
import { Button } from '@components';
import { Checkbox } from 'antd';
import styled from 'styled-components';
import { Icon } from '@components/components/Icon';
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 {
formLabelTextStyles,
inputPlaceholderTextStyles,
@ -40,7 +46,7 @@ export const SelectLabelContainer = styled.div({
gap: spacing.xsm,
lineHeight: typography.lineHeights.none,
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 }) => ({
...getDropdownStyle(),
borderRadius: radius.md,
background: colors.white,
zIndex: zIndices.dropdown,
@ -220,8 +227,8 @@ export const ArrowIcon = styled.span<{ isOpen: boolean }>(({ isOpen }) => ({
export const StyledCheckbox = styled(Checkbox)({
'.ant-checkbox-checked:not(.ant-checkbox-disabled) .ant-checkbox-inner': {
backgroundColor: colors.violet[500],
borderColor: `${colors.violet[500]} !important`,
backgroundColor: `${(props) => props.theme.styles['primary-color']}`,
borderColor: `${(props) => props.theme.styles['primary-color']} !important`,
},
});

View File

@ -109,11 +109,11 @@ export const getSelectStyle = (props: SelectStyleProps) => {
const baseStyle = {
borderRadius: radius.md,
border: `1px solid ${isDisabled ? colors.gray[1800] : colors.gray[100]}`,
border: `1px solid ${colors.gray[100]}`,
fontFamily: typography.fonts.body,
backgroundColor: isDisabled ? colors.gray[1500] : colors.white,
color: isDisabled ? colors.gray[300] : colors.gray[600],
cursor: isDisabled || isReadOnly ? 'not-allowed' : 'pointer',
backgroundColor: isDisabled ? colors.gray[1500] : 'initial',
boxShadow: '0px 1px 2px 0px rgba(33, 23, 95, 0.07)',
textWrap: 'nowrap',
@ -125,7 +125,7 @@ export const getSelectStyle = (props: SelectStyleProps) => {
...(isOpen
? {
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,
};
};
export const getDropdownStyle = () => {
const baseStyle = {
fontFamily: typography.fonts.body,
};
return { ...baseStyle };
};

View File

@ -38,7 +38,7 @@ export const StyledIcon = styled(Icon)({
export const TextAreaContainer = styled.div(
({ isSuccess, warning, isDisabled, isInvalid }: TextAreaProps) => ({
border: `${borders['1px']} ${getStatusColors(isSuccess, warning, isInvalid)}`,
backgroundColor: isDisabled ? colors.gray[100] : colors.white,
backgroundColor: isDisabled ? colors.gray[1500] : colors.white,
}),
{
...defaultFlexStyles,
@ -78,6 +78,10 @@ export const TextAreaField = styled.textarea<{ icon?: IconNames }>(({ icon }) =>
'&::placeholder': {
...inputPlaceholderTextStyles,
},
'&:disabled': {
backgroundColor: colors.gray[1500],
},
}));
export const Label = styled.div({

View File

@ -11,6 +11,7 @@ export * from './components/Button';
export * from './components/CalendarChart';
export * from './components/Card';
export * from './components/Checkbox';
export * from './components/ColorPicker';
export * from './components/DatePicker';
export * from './components/Drawer';
export * from './components/Dropdown';
@ -29,6 +30,7 @@ export * from './components/Pills';
export * from './components/Popover';
export * from './components/SearchBar';
export * from './components/Select';
export * from './components/StructuredPopover';
export * from './components/Switch';
export * from './components/Tabs';
export * from './components/Table';
@ -38,5 +40,3 @@ export * from './components/Timeline';
export * from './components/Tooltip';
export * from './components/Utils';
export * from './components/WhiskerChart';
export * from './components/ColorPicker';
export * from './components/StructuredPopover';

View File

@ -5,7 +5,6 @@ import { Theme } from '@conf/theme/types';
import { ColorOptions, DEFAULT_VALUE, FontSizeOptions, MiscColorOptions, RotationOptions } from './config';
import { foundations } from './foundations';
import { semanticTokens } from './semantic-tokens';
const { colors, typography, transform } = foundations;
/*
@ -67,5 +66,5 @@ export const getStatusColors = (isSuccess?: boolean, warning?: string, isInvalid
if (warning) {
return colors.yellow[600];
}
return semanticTokens.colors['border-color'];
return colors.gray[100];
};

View File

@ -2,7 +2,7 @@ import { Tooltip } from '@components';
import { Select, Tag } from 'antd';
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 { useGetEntitiesLazyQuery } from '@graphql/entity.generated';
@ -173,7 +173,7 @@ export const EntitySearchInput = ({
style={optionStyle}
data-testid={`${result.entity.urn}-entity-search-input-result`}
>
<EntitySearchInputResult entity={result.entity} />
<EntitySearchInputResultV2 entity={result.entity} />
</Select.Option>
))}
</Select>

View File

@ -1,7 +1,8 @@
import { Icon, Text } from '@components';
import { Text } from '@components';
import React from 'react';
import styled from 'styled-components';
import EntityRegistry from '@app/entityV2/EntityRegistry';
import { getDisplayedEntityType } from '@app/entityV2/shared/containers/profile/header/utils';
import ContextPath from '@app/previewV2/ContextPath';
import { useEntityRegistry } from '@app/useEntityRegistry';
@ -17,6 +18,7 @@ const Wrapper = styled.div`
const TextWrapper = styled.div`
display: flex;
flex-direction: column;
// TODO: Add this as a prop if needed
max-width: 600px;
`;
@ -30,7 +32,7 @@ type Props = {
};
export default function EntitySearchInputResultV2({ entity }: Props) {
const entityRegistry = useEntityRegistry();
const entityRegistry = useEntityRegistry() as EntityRegistry;
const properties = entityRegistry.getGenericEntityProperties(entity.type, entity);
const platformIcon = properties?.platform?.properties?.logoUrl;
@ -38,10 +40,9 @@ export default function EntitySearchInputResultV2({ entity }: Props) {
return (
<Wrapper>
{!platformIcon && <Icon size="4xl" source="phosphor" icon="Placeholder" />}
{platformIcon && <IconContainer src={platformIcon} />}
<TextWrapper>
<Text size="lg">{entityRegistry.getDisplayName(entity.type, entity)}</Text>
<Text size="md">{entityRegistry.getDisplayName(entity.type, entity)}</Text>
<ContextPath
entityType={entity.type}
displayedEntityType={displayedEntityType}

View File

@ -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>
);
};