From 9deb900344c8758fd1d7c97e06f474e8a8b19a03 Mon Sep 17 00:00:00 2001 From: Gabe Lyons Date: Thu, 22 Sep 2022 10:56:55 -0700 Subject: [PATCH 1/6] adding select value modal --- .../search/action/GlossaryTermsDropdown.tsx | 1 - .../styled/search/action/TagsDropdown.tsx | 1 - .../Container/ContainerSelectModal.tsx | 186 +++++++++++++++ .../profile/sidebar/Domain/SetDomainModal.tsx | 23 +- .../sidebar/Ownership/EditOwnersModal.tsx | 92 ++++++-- .../sidebar/Platform/SelectPlatformModal.tsx | 2 +- .../search/AdvancedFilterSelectValueModal.tsx | 211 ++++++++++++++++++ .../src/app/search/ChooseEntityTypeModal.tsx | 48 ++++ .../src/app/search/EditTextModal.tsx | 33 +++ .../src/app/shared/tags/AddTagsTermsModal.tsx | 63 +++--- .../src/app/shared/tags/TagTermGroup.tsx | 1 - .../src/app/shared/tags/TagTermLabel.tsx | 36 +++ 12 files changed, 643 insertions(+), 54 deletions(-) create mode 100644 datahub-web-react/src/app/entity/shared/containers/profile/sidebar/Container/ContainerSelectModal.tsx create mode 100644 datahub-web-react/src/app/search/AdvancedFilterSelectValueModal.tsx create mode 100644 datahub-web-react/src/app/search/ChooseEntityTypeModal.tsx create mode 100644 datahub-web-react/src/app/search/EditTextModal.tsx create mode 100644 datahub-web-react/src/app/shared/tags/TagTermLabel.tsx diff --git a/datahub-web-react/src/app/entity/shared/components/styled/search/action/GlossaryTermsDropdown.tsx b/datahub-web-react/src/app/entity/shared/components/styled/search/action/GlossaryTermsDropdown.tsx index 6c48164121478..7206abed305c4 100644 --- a/datahub-web-react/src/app/entity/shared/components/styled/search/action/GlossaryTermsDropdown.tsx +++ b/datahub-web-react/src/app/entity/shared/components/styled/search/action/GlossaryTermsDropdown.tsx @@ -48,7 +48,6 @@ export default function GlossaryTermsDropdown({ urns, disabled = false, refetch resourceUrn: urn, }))} operationType={operationType} - entityType={EntityType.Dataset} // TODO REMOVE /> )} diff --git a/datahub-web-react/src/app/entity/shared/components/styled/search/action/TagsDropdown.tsx b/datahub-web-react/src/app/entity/shared/components/styled/search/action/TagsDropdown.tsx index 2c46db8a70213..411ee62f64cca 100644 --- a/datahub-web-react/src/app/entity/shared/components/styled/search/action/TagsDropdown.tsx +++ b/datahub-web-react/src/app/entity/shared/components/styled/search/action/TagsDropdown.tsx @@ -47,7 +47,6 @@ export default function TagsDropdown({ urns, disabled = false, refetch }: Props) resources={urns.map((urn) => ({ resourceUrn: urn, }))} - entityType={EntityType.DataFlow} operationType={operationType} /> )} diff --git a/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/Container/ContainerSelectModal.tsx b/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/Container/ContainerSelectModal.tsx new file mode 100644 index 0000000000000..912fc8bfdf6b8 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/Container/ContainerSelectModal.tsx @@ -0,0 +1,186 @@ +import { Button, Form, Modal, Select, Tag, Tooltip } from 'antd'; +import React, { ReactNode, useRef, useState } from 'react'; +import styled from 'styled-components/macro'; +import { useGetSearchResultsLazyQuery } from '../../../../../../../graphql/search.generated'; +import { Container, Entity, EntityType } from '../../../../../../../types.generated'; +import { useEnterKeyListener } from '../../../../../../shared/useEnterKeyListener'; + +type Props = { + onCloseModal: () => void; + defaultValues?: { urn: string; entity?: Entity | null }[]; + onOkOverride?: (result: string[]) => void; + titleOverride?: string; +}; + +type SelectedContainer = { + entity?: Entity | null; + urn: string; +}; + +const StyleTag = styled(Tag)` + padding: 0px 7px; + margin-right: 3px; + display: flex; + justify-content: start; + align-items: center; +`; + +const PreviewImage = styled.img` + max-height: 18px; + width: auto; + object-fit: contain; + background-color: transparent; + margin-right: 4px; +`; + +export const SelectContainerModal = ({ onCloseModal, defaultValues, onOkOverride, titleOverride }: Props) => { + const [containerSearch, { data: platforSearchData }] = useGetSearchResultsLazyQuery(); + const containerSearchResults = + platforSearchData?.search?.searchResults?.map((searchResult) => searchResult.entity) || []; + + const [selectedContainers, setSelectedContainers] = useState(defaultValues); + + const inputEl = useRef(null); + + const onModalClose = () => { + onCloseModal(); + }; + + const handleSearch = (text: string) => { + containerSearch({ + variables: { + input: { + type: EntityType.Container, + query: text, + start: 0, + count: 5, + }, + }, + }); + }; + + // Renders a search result in the select dropdown. + const renderSearchResult = (entity: Container) => { + const displayName = entity.properties?.name || entity.properties?.qualifiedName || entity.urn; + const truncatedDisplayName = displayName.length > 25 ? `${displayName.slice(0, 25)}...` : displayName; + return ( + + + {truncatedDisplayName} + + ); + }; + + const containerSearchOptions = containerSearchResults?.map((result) => { + return ( + + {renderSearchResult(result as Container)} + + ); + }); + + const onSelectContainer = (newValue: { value: string; label: ReactNode }) => { + const newUrn = newValue.value; + + if (inputEl && inputEl.current) { + (inputEl.current as any).blur(); + } + + const filteredContainers = + containerSearchResults?.filter((entity) => entity.urn === newUrn).map((entity) => entity) || []; + + if (filteredContainers.length) { + const container = filteredContainers[0] as Container; + setSelectedContainers([ + ...(selectedContainers || []), + { + entity: container, + urn: newUrn, + }, + ]); + } + }; + + const onDeselectContainer = (val) => { + setSelectedContainers(selectedContainers?.filter((container) => container.urn !== val.value)); + }; + + const onOk = async () => { + if (!selectedContainers) { + return; + } + + if (onOkOverride) { + onOkOverride(selectedContainers?.map((container) => container.urn)); + } + }; + + // Handle the Enter press + useEnterKeyListener({ + querySelectorToExecuteClick: '#setContainerButton', + }); + + const tagRender = (props) => { + // eslint-disable-next-line react/prop-types + const { label, closable, onClose } = props; + const onPreventMouseDown = (event) => { + event.preventDefault(); + event.stopPropagation(); + }; + return ( + + {label} + + ); + }; + + return ( + + + + + } + > +
+ + + +
+
+ ); +}; diff --git a/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/Domain/SetDomainModal.tsx b/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/Domain/SetDomainModal.tsx index da9c91117893b..39014a28581a8 100644 --- a/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/Domain/SetDomainModal.tsx +++ b/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/Domain/SetDomainModal.tsx @@ -14,6 +14,9 @@ type Props = { urns: string[]; onCloseModal: () => void; refetch?: () => Promise; + defaultValue?: { urn: string; entity?: Entity | null }; + onOkOverride?: (result: string) => void; + titleOverride?: string; }; type SelectedDomain = { @@ -30,10 +33,18 @@ const StyleTag = styled(Tag)` align-items: center; `; -export const SetDomainModal = ({ urns, onCloseModal, refetch }: Props) => { +export const SetDomainModal = ({ urns, onCloseModal, refetch, defaultValue, onOkOverride, titleOverride }: Props) => { const entityRegistry = useEntityRegistry(); const [inputValue, setInputValue] = useState(''); - const [selectedDomain, setSelectedDomain] = useState(undefined); + const [selectedDomain, setSelectedDomain] = useState( + defaultValue + ? { + displayName: entityRegistry.getDisplayName(EntityType.Domain, defaultValue?.entity), + type: EntityType.Domain, + urn: defaultValue?.urn, + } + : undefined, + ); const [domainSearch, { data: domainSearchData }] = useGetSearchResultsLazyQuery(); const domainSearchResults = domainSearchData?.search?.searchResults?.map((searchResult) => searchResult.entity) || []; @@ -100,6 +111,12 @@ export const SetDomainModal = ({ urns, onCloseModal, refetch }: Props) => { if (!selectedDomain) { return; } + + if (onOkOverride) { + onOkOverride(selectedDomain?.urn); + return; + } + batchSetDomainMutation({ variables: { input: { @@ -149,7 +166,7 @@ export const SetDomainModal = ({ urns, onCloseModal, refetch }: Props) => { return ( void; refetch?: () => Promise; entityType?: EntityType; // Only used for tracking events + onOkOverride?: (result: SelectedOwner[]) => void; + title?: string; + defaultValues?: { urn: string; entity?: Entity | null }[]; }; // value: {ownerUrn: string, ownerEntityType: EntityType} type SelectedOwner = { - label: string; - value; + label: string | React.ReactNode; + value: { + ownerUrn: string; + ownerEntityType: EntityType; + }; }; export const EditOwnersModal = ({ @@ -57,13 +63,53 @@ export const EditOwnersModal = ({ onCloseModal, refetch, entityType, + onOkOverride, + title, + defaultValues, }: Props) => { const entityRegistry = useEntityRegistry(); + + // Renders a search result in the select dropdown. + const renderSearchResult = (entity: Entity) => { + const avatarUrl = + entity.type === EntityType.CorpUser + ? (entity as CorpUser).editableProperties?.pictureLink || undefined + : undefined; + const displayName = entityRegistry.getDisplayName(entity.type, entity); + return ( + + + + ); + }; + + const renderDropdownResult = (entity: Entity) => { + const avatarUrl = + entity.type === EntityType.CorpUser + ? (entity as CorpUser).editableProperties?.pictureLink || undefined + : undefined; + const displayName = entityRegistry.getDisplayName(entity.type, entity); + return ; + }; + + const defaultValuesToSelectedOwners = (vals: { urn: string; entity?: Entity | null }[]): SelectedOwner[] => { + return vals.map((defaultValue) => ({ + label: defaultValue.entity ? renderDropdownResult(defaultValue.entity) : defaultValue.urn, + value: { + ownerUrn: defaultValue.urn, + ownerEntityType: EntityType.CorpUser, + }, + })); + }; + const [inputValue, setInputValue] = useState(''); const [batchAddOwnersMutation] = useBatchAddOwnersMutation(); const [batchRemoveOwnersMutation] = useBatchRemoveOwnersMutation(); const ownershipTypes = OWNERSHIP_DISPLAY_TYPES; - const [selectedOwners, setSelectedOwners] = useState([]); + const [selectedOwners, setSelectedOwners] = useState( + defaultValuesToSelectedOwners(defaultValues || []), + ); + console.log({ selectedOwners }); const [selectedOwnerType, setSelectedOwnerType] = useState(defaultOwnerType || OwnershipType.None); // User and group dropdown search results! @@ -101,20 +147,6 @@ export const EditOwnersModal = ({ handleSearch(EntityType.CorpGroup, text, groupSearch); }; - // Renders a search result in the select dropdown. - const renderSearchResult = (entity: Entity) => { - const avatarUrl = - entity.type === EntityType.CorpUser - ? (entity as CorpUser).editableProperties?.pictureLink || undefined - : undefined; - const displayName = entityRegistry.getDisplayName(entity.type, entity); - return ( - - - - ); - }; - const ownerResult = !inputValue || inputValue.length === 0 ? recommendedData : combinedSearchResults; const ownerSearchOptions = ownerResult?.map((result) => { @@ -149,7 +181,7 @@ export const EditOwnersModal = ({ label: selectedValue.value, value: { ownerUrn: selectedValue.value, - ownerEntityType, + ownerEntityType: ownerEntityType as unknown as EntityType, }, }, ]; @@ -159,8 +191,15 @@ export const EditOwnersModal = ({ // When a owner search result is deselected, remove the Owner const onDeselectOwner = (selectedValue: { key: string; label: React.ReactNode; value: string }) => { + console.log({ + deselecting: 'deselecting', + selectedValue, + selectedOwners, + }); setInputValue(''); - const newValues = selectedOwners.filter((owner) => owner.label !== selectedValue.value); + const newValues = selectedOwners.filter( + (owner) => owner.label !== selectedValue.value && owner.value.ownerUrn !== selectedValue.value, + ); setSelectedOwners(newValues); }; @@ -251,6 +290,12 @@ export const EditOwnersModal = ({ if (selectedOwners.length === 0) { return; } + + if (onOkOverride) { + onOkOverride(selectedOwners); + return; + } + const inputs = selectedOwners.map((selectedActor) => { const input = { ownerUrn: selectedActor.value.ownerUrn, @@ -273,7 +318,7 @@ export const EditOwnersModal = ({ return ( ({ + key: owner.value.ownerUrn, + value: owner.value.ownerUrn, + label: owner.label, + }))} > {ownerSearchOptions} diff --git a/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/Platform/SelectPlatformModal.tsx b/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/Platform/SelectPlatformModal.tsx index f04e7fcc74b8c..978996da93a19 100644 --- a/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/Platform/SelectPlatformModal.tsx +++ b/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/Platform/SelectPlatformModal.tsx @@ -102,7 +102,7 @@ export const SelectPlatformModal = ({ onCloseModal, defaultValues, onOkOverride, }; const onDeselectPlatform = (val) => { - setSelectedPlatforms(selectedPlatforms?.filter((platform) => platform.urn !== val)); + setSelectedPlatforms(selectedPlatforms?.filter((platform) => platform.urn !== val.value)); }; const onOk = async () => { diff --git a/datahub-web-react/src/app/search/AdvancedFilterSelectValueModal.tsx b/datahub-web-react/src/app/search/AdvancedFilterSelectValueModal.tsx new file mode 100644 index 0000000000000..206953e5dd6d4 --- /dev/null +++ b/datahub-web-react/src/app/search/AdvancedFilterSelectValueModal.tsx @@ -0,0 +1,211 @@ +import React from 'react'; +import { FacetMetadata, EntityType } from '../../types.generated'; +import { SelectContainerModal } from '../entity/shared/containers/profile/sidebar/Container/ContainerSelectModal'; +import { SetDomainModal } from '../entity/shared/containers/profile/sidebar/Domain/SetDomainModal'; +import { EditOwnersModal } from '../entity/shared/containers/profile/sidebar/Ownership/EditOwnersModal'; +import { SelectPlatformModal } from '../entity/shared/containers/profile/sidebar/Platform/SelectPlatformModal'; +import EditTagTermsModal from '../shared/tags/AddTagsTermsModal'; +import { ChooseEntityTypeModal } from './ChooseEntityTypeModal'; +import { EditTextModal } from './EditTextModal'; + +type Props = { + facet?: FacetMetadata | null; + filterField: string; + onSelect: (values: string[]) => void; + onCloseModal: () => void; + initialValues?: string[]; +}; + +export const AdvancedFilterSelectValueModal = ({ + filterField, + onSelect, + onCloseModal, + initialValues, + facet, +}: Props) => { + if (filterField === 'owners') { + return ( + ({ + urn, + entity: facet?.aggregations.find((aggregation) => aggregation.value === urn)?.entity, + }))} + onCloseModal={onCloseModal} + hideOwnerType + onOkOverride={(owners) => { + onSelect(owners.map((owner) => owner.value.ownerUrn)); + onCloseModal(); + }} + /> + ); + } + if (filterField === 'domains') { + return ( + ({ + urn, + entity: facet?.aggregations.find((aggregation) => aggregation.value === urn)?.entity, + }))?.[0] + } + onCloseModal={onCloseModal} + onOkOverride={(domainUrn) => { + onSelect([domainUrn]); + onCloseModal(); + }} + /> + ); + } + + if (filterField === 'container') { + return ( + ({ + urn, + entity: facet?.aggregations.find((aggregation) => aggregation.value === urn)?.entity, + }))} + onCloseModal={onCloseModal} + onOkOverride={(containerUrns) => { + onSelect(containerUrns); + onCloseModal(); + }} + /> + ); + } + + if (filterField === 'platform') { + return ( + ({ + urn, + entity: facet?.aggregations.find((aggregation) => aggregation.value === urn)?.entity, + }))} + titleOverride="Select Platform" + onCloseModal={onCloseModal} + onOkOverride={(platformUrns) => { + onSelect(platformUrns); + onCloseModal(); + }} + /> + ); + } + + if (filterField === 'fieldPaths') { + return ( + { + onSelect([newValue]); + onCloseModal(); + }} + /> + ); + } + + if (filterField === 'description' || filterField === 'fieldDescriptions') { + return ( + { + onSelect([newValue]); + onCloseModal(); + }} + /> + ); + } + + if (filterField === 'origin') { + return ( + { + onSelect([newValue]); + onCloseModal(); + }} + /> + ); + } + + if (filterField === 'typeNames') { + return ( + { + onSelect([newValue]); + onCloseModal(); + }} + /> + ); + } + + if (filterField === 'entity') { + return ( + { + onSelect([newValue]); + onCloseModal(); + }} + /> + ); + } + + if (filterField === 'tags' || filterField === 'fieldTags') { + return ( + { + onSelect(urns); + onCloseModal(); + }} + defaultValues={initialValues?.map((urn) => ({ + urn, + entity: facet?.aggregations.find((aggregation) => aggregation.value === urn)?.entity, + }))} + /> + ); + } + + if (filterField === 'removed') { + onSelect(['true']); + onCloseModal(); + } + + if (filterField === 'glossaryTerms' || filterField === 'fieldGlossaryTerms') { + return ( + { + onSelect(urns); + onCloseModal(); + }} + defaultValues={initialValues?.map((urn) => ({ + urn, + entity: facet?.aggregations.find((aggregation) => aggregation.value === urn)?.entity, + }))} + /> + ); + } + return null; +}; diff --git a/datahub-web-react/src/app/search/ChooseEntityTypeModal.tsx b/datahub-web-react/src/app/search/ChooseEntityTypeModal.tsx new file mode 100644 index 0000000000000..279aa6f6dbb91 --- /dev/null +++ b/datahub-web-react/src/app/search/ChooseEntityTypeModal.tsx @@ -0,0 +1,48 @@ +import { Button, Modal, Select } from 'antd'; +import React, { useState } from 'react'; +import { useEntityRegistry } from '../useEntityRegistry'; + +type Props = { + onCloseModal: () => void; + onOkOverride?: (result: string) => void; + title?: string; + defaultValue?: string; +}; + +const { Option } = Select; + +export const ChooseEntityTypeModal = ({ defaultValue, onCloseModal, onOkOverride, title }: Props) => { + const entityRegistry = useEntityRegistry(); + const entityTypes = entityRegistry.getSearchEntityTypes(); + + const [stagedValue, setStagedValue] = useState(defaultValue || entityTypes[0]); + + return ( + + + + + } + > + + + ); +}; diff --git a/datahub-web-react/src/app/search/EditTextModal.tsx b/datahub-web-react/src/app/search/EditTextModal.tsx new file mode 100644 index 0000000000000..bda7a4c63e56d --- /dev/null +++ b/datahub-web-react/src/app/search/EditTextModal.tsx @@ -0,0 +1,33 @@ +import { Button, Input, Modal } from 'antd'; +import React, { useState } from 'react'; + +type Props = { + onCloseModal: () => void; + onOkOverride?: (result: string) => void; + title?: string; + defaultValue?: string; +}; + +export const EditTextModal = ({ defaultValue, onCloseModal, onOkOverride, title }: Props) => { + const [stagedValue, setStagedValue] = useState(defaultValue || ''); + return ( + + + + + } + > + setStagedValue(e.target.value)} value={stagedValue} /> + + ); +}; diff --git a/datahub-web-react/src/app/shared/tags/AddTagsTermsModal.tsx b/datahub-web-react/src/app/shared/tags/AddTagsTermsModal.tsx index 6a949bee34401..937574d56913c 100644 --- a/datahub-web-react/src/app/shared/tags/AddTagsTermsModal.tsx +++ b/datahub-web-react/src/app/shared/tags/AddTagsTermsModal.tsx @@ -12,13 +12,12 @@ import { useBatchRemoveTermsMutation, } from '../../../graphql/mutations.generated'; import { useEnterKeyListener } from '../useEnterKeyListener'; -import TermLabel from '../TermLabel'; -import TagLabel from '../TagLabel'; import GlossaryBrowser from '../../glossary/GlossaryBrowser/GlossaryBrowser'; import ClickOutside from '../ClickOutside'; import { useEntityRegistry } from '../../useEntityRegistry'; import { useGetRecommendations } from '../recommendation'; import { FORBIDDEN_URN_CHARS_REGEX } from '../../entity/shared/utils'; +import { TagTermLabel } from './TagTermLabel'; export enum OperationType { ADD, @@ -29,10 +28,10 @@ type EditTagsModalProps = { visible: boolean; onCloseModal: () => void; resources: ResourceRefInput[]; - // eslint-disable-next-line - entityType: EntityType; type?: EntityType; operationType?: OperationType; + defaultValues?: { urn: string; entity?: Entity | null }[]; + onOkOverride?: (result: string[]) => void; }; const TagSelect = styled(Select)` @@ -80,14 +79,33 @@ export default function EditTagTermsModal({ resources, type = EntityType.Tag, operationType = OperationType.ADD, + defaultValues = [], + onOkOverride, }: EditTagsModalProps) { + console.log({ defaultValues }); const entityRegistry = useEntityRegistry(); const [inputValue, setInputValue] = useState(''); const [showCreateModal, setShowCreateModal] = useState(false); const [disableAction, setDisableAction] = useState(false); - const [urns, setUrns] = useState([]); - const [selectedTerms, setSelectedTerms] = useState([]); - const [selectedTags, setSelectedTags] = useState([]); + const [urns, setUrns] = useState(defaultValues.map((defaultValue) => defaultValue.urn)); + const [selectedTerms, setSelectedTerms] = useState( + type === EntityType.GlossaryTerm + ? defaultValues.map((defaultValue) => ({ + urn: defaultValue.urn, + component: , + })) + : [], + ); + + const [selectedTags, setSelectedTags] = useState( + type === EntityType.Tag + ? defaultValues.map((defaultValue) => ({ + urn: defaultValue.urn, + component: , + })) + : [], + ); + const [isFocusedOnInput, setIsFocusedOnInput] = useState(false); const [batchAddTagsMutation] = useBatchAddTagsMutation(); @@ -118,16 +136,7 @@ export default function EditTagTermsModal({ const renderSearchResult = (entity: Entity) => { const displayName = entity.type === EntityType.Tag ? (entity as Tag).name : entityRegistry.getDisplayName(entity.type, entity); - const tagOrTermComponent = - entity.type === EntityType.Tag ? ( - - ) : ( - - ); + const tagOrTermComponent = ; return ( {tagOrTermComponent} @@ -209,18 +218,15 @@ export default function EditTagTermsModal({ const selectedSearchOption = tagSearchOptions?.find((option) => option.props.value === urn); const selectedTagOption = tagResult?.find((tag) => tag.urn === urn); setUrns(newUrns); - setSelectedTerms([...selectedTerms, { urn, component: }]); + setSelectedTerms([ + ...selectedTerms, + { urn, component: }, + ]); setSelectedTags([ ...selectedTags, { urn, - component: ( - - ), + component: , }, ]); if (inputEl && inputEl.current) { @@ -368,6 +374,11 @@ export default function EditTagTermsModal({ // Function to handle the modal action's const onOk = () => { + if (onOkOverride) { + onOkOverride(urns); + return; + } + if (!resources) { onCloseModal(); return; @@ -385,7 +396,7 @@ export default function EditTagTermsModal({ setIsFocusedOnInput(false); const newUrns = [...(urns || []), urn]; setUrns(newUrns); - setSelectedTerms([...selectedTerms, { urn, component: }]); + setSelectedTerms([...selectedTerms, { urn, component: }]); } function clearInput() { diff --git a/datahub-web-react/src/app/shared/tags/TagTermGroup.tsx b/datahub-web-react/src/app/shared/tags/TagTermGroup.tsx index 7fe50993b938c..4cf338aac5f80 100644 --- a/datahub-web-react/src/app/shared/tags/TagTermGroup.tsx +++ b/datahub-web-react/src/app/shared/tags/TagTermGroup.tsx @@ -371,7 +371,6 @@ export default function TagTermGroup({ subResourceType: entitySubresource ? SubResourceType.DatasetField : null, }, ]} - entityType={entityType} /> )} diff --git a/datahub-web-react/src/app/shared/tags/TagTermLabel.tsx b/datahub-web-react/src/app/shared/tags/TagTermLabel.tsx new file mode 100644 index 0000000000000..0c1117ef42718 --- /dev/null +++ b/datahub-web-react/src/app/shared/tags/TagTermLabel.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Entity, EntityType, Tag } from '../../../types.generated'; +import { useEntityRegistry } from '../../useEntityRegistry'; +import TagLabel from '../TagLabel'; +import TermLabel from '../TermLabel'; + +type Props = { + // default behavior is to accept an entity and render label based on that + entity?: Entity | null; + + // if no entity is available, for terms just a name may be provided + termName?: string; +}; + +export const TagTermLabel = ({ entity, termName }: Props) => { + const entityRegistry = useEntityRegistry(); + + if (entity?.type === EntityType.Tag) { + return ( + + ); + } + + if (entity?.type === EntityType.GlossaryTerm) { + return ; + } + + if (termName) { + return ; + } + return null; +}; From 0a07a69110ba190d1f2cd7ad8a8164503a1a4eb5 Mon Sep 17 00:00:00 2001 From: Gabe Lyons Date: Thu, 22 Sep 2022 15:37:01 -0700 Subject: [PATCH 2/6] expanding gql model and adding advanced search filter component --- .../assertion/AssertionRunEventResolver.java | 4 +- .../src/main/resources/search.graphql | 400 ++++++++++-------- .../auth/ListAccessTokensResolverTest.java | 5 +- .../src/app/search/AdvancedSearchFilter.tsx | 126 ++++++ .../AdvancedSearchFilterConditionSelect.tsx | 94 ++++ .../src/app/search/SearchFilterLabel.tsx | 41 +- .../src/app/search/utils/constants.ts | 36 ++ 7 files changed, 507 insertions(+), 199 deletions(-) create mode 100644 datahub-web-react/src/app/search/AdvancedSearchFilter.tsx create mode 100644 datahub-web-react/src/app/search/AdvancedSearchFilterConditionSelect.tsx diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/assertion/AssertionRunEventResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/assertion/AssertionRunEventResolver.java index 7d787b20f019b..630e6718ba0da 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/assertion/AssertionRunEventResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/assertion/AssertionRunEventResolver.java @@ -1,5 +1,6 @@ package com.linkedin.datahub.graphql.resolvers.assertion; +import com.google.common.collect.ImmutableList; import com.linkedin.datahub.graphql.QueryContext; import com.linkedin.datahub.graphql.generated.Assertion; import com.linkedin.datahub.graphql.generated.AssertionResultType; @@ -8,6 +9,7 @@ import com.linkedin.datahub.graphql.generated.AssertionRunStatus; import com.linkedin.datahub.graphql.generated.FacetFilterInput; import com.linkedin.datahub.graphql.generated.FilterInput; +import com.linkedin.datahub.graphql.generated.SearchCondition; import com.linkedin.datahub.graphql.types.dataset.mappers.AssertionRunEventMapper; import com.linkedin.entity.client.EntityClient; import com.linkedin.metadata.Constants; @@ -100,7 +102,7 @@ private Filter buildFilter(@Nullable FilterInput filtersInput, @Nullable final S } List facetFilters = new ArrayList<>(); if (status != null) { - facetFilters.add(new FacetFilterInput("status", status)); + facetFilters.add(new FacetFilterInput("status", status, ImmutableList.of(status), false, SearchCondition.EQUAL)); } if (filtersInput != null) { facetFilters.addAll(filtersInput.getAnd()); diff --git a/datahub-graphql-core/src/main/resources/search.graphql b/datahub-graphql-core/src/main/resources/search.graphql index 015a964c90b96..5dc7ec43bd5fd 100644 --- a/datahub-graphql-core/src/main/resources/search.graphql +++ b/datahub-graphql-core/src/main/resources/search.graphql @@ -12,7 +12,9 @@ extend type Query { """ Search across the results of a graph query on a node """ - searchAcrossLineage(input: SearchAcrossLineageInput!): SearchAcrossLineageResults + searchAcrossLineage( + input: SearchAcrossLineageInput! + ): SearchAcrossLineageResults """ Autocomplete a search query against a specific DataHub Entity Type @@ -22,7 +24,9 @@ extend type Query { """ Autocomplete a search query against a specific set of DataHub Entity Types """ - autoCompleteForMultiple(input: AutoCompleteMultipleInput!): AutoCompleteMultipleResults + autoCompleteForMultiple( + input: AutoCompleteMultipleInput! + ): AutoCompleteMultipleResults """ Hierarchically browse a specific type of DataHub Entity by path @@ -43,27 +47,27 @@ input SearchInput { """ The Metadata Entity type to be searched against """ - type: EntityType! + type: EntityType! - """ - The raw query string - """ - query: String! + """ + The raw query string + """ + query: String! - """ - The offset of the result set - """ - start: Int + """ + The offset of the result set + """ + start: Int - """ - The number of entities to include in result set - """ - count: Int + """ + The number of entities to include in result set + """ + count: Int - """ - Facet filters to apply to search results - """ - filters: [FacetFilterInput!] + """ + Facet filters to apply to search results + """ + filters: [FacetFilterInput!] } """ @@ -73,67 +77,67 @@ input SearchAcrossEntitiesInput { """ Entity types to be searched. If this is not provided, all entities will be searched. """ - types: [EntityType!] + types: [EntityType!] - """ - The query string - """ - query: String! + """ + The query string + """ + query: String! - """ - The starting point of paginated results - """ - start: Int + """ + The starting point of paginated results + """ + start: Int - """ - The number of elements included in the results - """ - count: Int + """ + The number of elements included in the results + """ + count: Int - """ - Faceted filters applied to search results - """ - filters: [FacetFilterInput!] + """ + Faceted filters applied to search results + """ + filters: [FacetFilterInput!] } """ Input arguments for a search query over the results of a multi-hop graph query """ input SearchAcrossLineageInput { - """ - Urn of the source node - """ - urn: String - - """ - The direction of the relationship, either incoming or outgoing from the source entity - """ - direction: LineageDirection! - - """ - Entity types to be searched. If this is not provided, all entities will be searched. - """ - types: [EntityType!] - - """ - The query string - """ - query: String - - """ - The starting point of paginated results - """ - start: Int - - """ - The number of elements included in the results - """ - count: Int - - """ - Faceted filters applied to search results - """ - filters: [FacetFilterInput!] + """ + Urn of the source node + """ + urn: String + + """ + The direction of the relationship, either incoming or outgoing from the source entity + """ + direction: LineageDirection! + + """ + Entity types to be searched. If this is not provided, all entities will be searched. + """ + types: [EntityType!] + + """ + The query string + """ + query: String + + """ + The starting point of paginated results + """ + start: Int + + """ + The number of elements included in the results + """ + count: Int + + """ + Faceted filters applied to search results + """ + filters: [FacetFilterInput!] } """ @@ -143,12 +147,44 @@ input FacetFilterInput { """ Name of field to filter by """ - field: String! + field: String! + + """ + Value of the field to filter by (soon to be deprecated) + """ + value: String! + + """ + Values of the field to filter by + """ + values: [String!] - """ - Value of the field to filter by - """ - value: String! + """ + If the filter should or should not be matched + """ + negated: Boolean + + """ + Condition for the values. How to If unset, assumed to be equality + """ + condition: SearchCondition +} + +enum SearchCondition { + """ + Represent the relation: String field contains value, e.g. name contains Profile + """ + CONTAIN + + """ + Represent the relation: field = value, e.g. platform = hdfs + """ + EQUAL + + """ + * Represent the relation: String field is one of the array values to, e.g. name in ["Profile", "Event"] + """ + IN } """ @@ -205,60 +241,60 @@ type SearchResult { Results returned by issueing a search across relationships query """ type SearchAcrossLineageResults { - """ - The offset of the result set - """ - start: Int! - - """ - The number of entities included in the result set - """ - count: Int! - - """ - The total number of search results matching the query and filters - """ - total: Int! - - """ - The search result entities - """ - searchResults: [SearchAcrossLineageResult!]! - - """ - Candidate facet aggregations used for search filtering - """ - facets: [FacetMetadata!] + """ + The offset of the result set + """ + start: Int! + + """ + The number of entities included in the result set + """ + count: Int! + + """ + The total number of search results matching the query and filters + """ + total: Int! + + """ + The search result entities + """ + searchResults: [SearchAcrossLineageResult!]! + + """ + Candidate facet aggregations used for search filtering + """ + facets: [FacetMetadata!] } """ Individual search result from a search across relationships query (has added metadata about the path) """ type SearchAcrossLineageResult { - """ - The resolved DataHub Metadata Entity matching the search query - """ - entity: Entity! - - """ - Insights about why the search result was matched - """ - insights: [SearchInsight!] - - """ - Matched field hint - """ - matchedFields: [MatchedField!]! - - """ - Optional list of entities between the source and destination node - """ - path: [Entity!] - - """ - Degree of relationship (number of hops to get to entity) - """ - degree: Int! + """ + The resolved DataHub Metadata Entity matching the search query + """ + entity: Entity! + + """ + Insights about why the search result was matched + """ + insights: [SearchInsight!] + + """ + Matched field hint + """ + matchedFields: [MatchedField!]! + + """ + Optional list of entities between the source and destination node + """ + path: [Entity!] + + """ + Degree of relationship (number of hops to get to entity) + """ + degree: Int! } """ @@ -280,15 +316,15 @@ type MatchedField { Insights about why a search result was returned or ranked in the way that it was """ type SearchInsight { - """ - The insight to display - """ - text: String! - - """ - An optional emoji to display in front of the text - """ - icon: String + """ + The insight to display + """ + text: String! + + """ + An optional emoji to display in front of the text + """ + icon: String } """ @@ -338,27 +374,27 @@ input AutoCompleteInput { """ Entity type to be autocompleted against """ - type: EntityType + type: EntityType - """ - The raw query string - """ - query: String! + """ + The raw query string + """ + query: String! - """ - An optional entity field name to autocomplete on - """ - field: String + """ + An optional entity field name to autocomplete on + """ + field: String - """ - The maximum number of autocomplete results to be returned - """ - limit: Int + """ + The maximum number of autocomplete results to be returned + """ + limit: Int - """ - Faceted filters applied to autocomplete results - """ - filters: [FacetFilterInput!] + """ + Faceted filters applied to autocomplete results + """ + filters: [FacetFilterInput!] } """ @@ -431,15 +467,15 @@ type AutoCompleteMultipleResults { The results returned on a single entity autocomplete query """ type AutoCompleteResults { - """ - The query string - """ - query: String! + """ + The query string + """ + query: String! - """ - The autocompletion results - """ - suggestions: [String!]! + """ + The autocompletion results + """ + suggestions: [String!]! """ A list of entities to render in autocomplete @@ -454,27 +490,27 @@ input BrowseInput { """ The browse entity type """ - type: EntityType! + type: EntityType! - """ - The browse path - """ - path: [String!] + """ + The browse path + """ + path: [String!] """ The starting point of paginated results """ - start: Int + start: Int """ The number of elements included in the results """ - count: Int + count: Int """ Faceted filters applied to browse results """ - filters: [FacetFilterInput!] + filters: [FacetFilterInput!] } """ @@ -484,32 +520,32 @@ type BrowseResults { """ The browse results """ - entities: [Entity!]! + entities: [Entity!]! - """ - The groups present at the provided browse path - """ - groups: [BrowseResultGroup!]! + """ + The groups present at the provided browse path + """ + groups: [BrowseResultGroup!]! """ The starting point of paginated results """ - start: Int! + start: Int! """ The number of elements included in the results """ - count: Int! + count: Int! """ The total number of browse results under the path with filters applied """ - total: Int! + total: Int! """ Metadata containing resulting browse groups """ - metadata: BrowseResultMetadata! + metadata: BrowseResultMetadata! } """ @@ -519,12 +555,12 @@ type BrowseResultMetadata { """ The provided path """ - path: [String!]! + path: [String!]! """ The total number of entities under the provided browse path """ - totalNumEntities: Long! + totalNumEntities: Long! } """ @@ -549,22 +585,22 @@ input BrowsePathsInput { """ The browse entity type """ - type: EntityType! + type: EntityType! """ The entity urn """ - urn: String! + urn: String! } """ A hierarchical entity path """ type BrowsePath { - """ - The components of the browse path - """ - path: [String!]! + """ + The components of the browse path + """ + path: [String!]! } """ diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/auth/ListAccessTokensResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/auth/ListAccessTokensResolverTest.java index 3f4986c7ebc07..1ef3c101e6c2a 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/auth/ListAccessTokensResolverTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/auth/ListAccessTokensResolverTest.java @@ -6,6 +6,7 @@ import com.linkedin.datahub.graphql.generated.FacetFilterInput; import com.linkedin.datahub.graphql.generated.ListAccessTokenInput; import com.linkedin.datahub.graphql.generated.ListAccessTokenResult; +import com.linkedin.datahub.graphql.generated.SearchCondition; import com.linkedin.entity.client.EntityClient; import com.linkedin.metadata.Constants; import graphql.schema.DataFetchingEnvironment; @@ -27,7 +28,7 @@ public void testGetSuccess() throws Exception { input.setStart(0); input.setCount(100); final ImmutableList filters = ImmutableList.of(new FacetFilterInput("actor", - "urn:li:corpuser:test")); + "urn:li:corpuser:test", ImmutableList.of("urn:li:corpuser:test"), false, SearchCondition.EQUAL)); input.setFilters(filters); Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(input); @@ -44,4 +45,4 @@ public void testGetSuccess() throws Exception { final ListAccessTokensResolver resolver = new ListAccessTokensResolver(mockClient); final ListAccessTokenResult listAccessTokenResult = resolver.get(mockEnv).get(); } -} \ No newline at end of file +} diff --git a/datahub-web-react/src/app/search/AdvancedSearchFilter.tsx b/datahub-web-react/src/app/search/AdvancedSearchFilter.tsx new file mode 100644 index 0000000000000..46446f7b37eae --- /dev/null +++ b/datahub-web-react/src/app/search/AdvancedSearchFilter.tsx @@ -0,0 +1,126 @@ +import { CloseOutlined } from '@ant-design/icons'; +import React, { useState } from 'react'; +import styled from 'styled-components'; + +import { FacetFilterInput, FacetMetadata } from '../../types.generated'; +import { ANTD_GRAY } from '../entity/shared/constants'; +import { AdvancedSearchFilterConditionSelect } from './AdvancedSearchFilterConditionSelect'; +import { SearchFilterLabel } from './SearchFilterLabel'; +import { AdvancedFilterSelectValueModal } from './AdvancedFilterSelectValueModal'; +import { FIELD_TO_LABEL } from './utils/constants'; + +type Props = { + facet: FacetMetadata; + filter: FacetFilterInput; + onClose: () => void; + onUpdate: (newValue: FacetFilterInput) => void; +}; + +const FilterContainer = styled.div` + box-shadow: 0px 0px 4px 0px #00000010; + border-radius: 10px; + border: 1px solid ${ANTD_GRAY[4]}; + padding: 5px; + margin: 4px; + :hover { + cursor: pointer; + background: ${ANTD_GRAY[2]}; + } +`; + +const FieldFilterSection = styled.span` + color: ${ANTD_GRAY[9]}; + padding: 4px; + display: flex; + justify-content: space-between; +`; + +const ValueFilterSection = styled.div` + :hover { + cursor: pointer; + } + border-top: 1px solid ${ANTD_GRAY[3]}; +`; + +const CloseSpan = styled.span` + :hover { + color: black; + } +`; + +const StyledSearchFilterLabel = styled.div` + margin: 4px; +`; + +const FilterFieldLabel = styled.span` + font-weight: 600; + margin-right: 2px; +`; + +const TEXT_FILTERS = ['fieldPaths']; + +export const AdvancedSearchFilter = ({ facet, filter, onClose, onUpdate }: Props) => { + const [isEditing, setIsEditing] = useState(false); + return ( + <> + { + setIsEditing(!isEditing); + }} + > + + + {FIELD_TO_LABEL[filter.field]} + + + { + e.preventDefault(); + e.stopPropagation(); + onClose(); + }} + tabIndex={0} + onKeyPress={onClose} + > + + + + + {TEXT_FILTERS.indexOf(filter.field) === -1 && + filter?.values?.map((value) => { + const matchedAggregation = facet?.aggregations?.find( + (aggregation) => aggregation.value === value, + ); + if (!matchedAggregation) return null; + + return ( + + + + ); + })} + {TEXT_FILTERS.indexOf(filter.field) !== -1 && filter?.values?.map((value) => {value})} + + + {isEditing && ( + setIsEditing(false)} + filterField={filter.field} + onSelect={(values) => { + const newFilter: FacetFilterInput = { + field: filter.field, + value: '', + values: values as string[], + condition: filter.condition, + negated: filter.negated || false, + }; + onUpdate(newFilter); + }} + initialValues={filter.values || []} + /> + )} + + ); +}; diff --git a/datahub-web-react/src/app/search/AdvancedSearchFilterConditionSelect.tsx b/datahub-web-react/src/app/search/AdvancedSearchFilterConditionSelect.tsx new file mode 100644 index 0000000000000..c9e40d54aaba5 --- /dev/null +++ b/datahub-web-react/src/app/search/AdvancedSearchFilterConditionSelect.tsx @@ -0,0 +1,94 @@ +import { Select } from 'antd'; +import React from 'react'; +import styled from 'styled-components/macro'; +// import styled from 'styled-components'; + +import { FacetFilterInput } from '../../types.generated'; +import { ANTD_GRAY } from '../entity/shared/constants'; +import { FIELDS_WHO_USE_CONTAINS_OPERATOR } from './utils/constants'; + +type Props = { + filter: FacetFilterInput; + onUpdate: (newValue: FacetFilterInput) => void; +}; + +const { Option } = Select; + +// We track which fields are collection fields for the purpose of printing the conditions +// in a more gramatically correct way. On the backend they are handled the same. +const filtersOnNonCollectionFields = [ + 'description', + 'fieldDescriptions', + 'fieldPaths', + 'removed', + 'typeNames', + 'entity', + 'domains', + 'origin', +]; + +function getLabelsForField(field: string) { + if (FIELDS_WHO_USE_CONTAINS_OPERATOR.indexOf(field) >= 0) { + return { + default: 'contains', + negated: 'does not contain', + }; + } + if (filtersOnNonCollectionFields.indexOf(field) >= 0) { + return { + default: 'equals', + negated: 'not equal', + }; + } + + // collection field + return { + default: 'is either of', + negated: 'is not', + }; +} + +const StyledSelect = styled(Select)` + border-radius: 5px; + background: ${ANTD_GRAY[4]}; + :hover { + background: ${ANTD_GRAY[4.5]}; + } +`; + +export const AdvancedSearchFilterConditionSelect = ({ filter, onUpdate }: Props) => { + const labelsForField = getLabelsForField(filter.field); + + const selectedValue = filter.negated ? 'negated' : 'default'; + + return ( + <> + { + e.preventDefault(); + e.stopPropagation(); + }} + showArrow={false} + bordered={false} + value={selectedValue} + onChange={(newValue) => { + if (newValue !== selectedValue) { + onUpdate({ + ...filter, + negated: newValue === 'negated', + }); + } + }} + size="small" + disabled={filter.field === 'entity'} + dropdownMatchSelectWidth={false} + > + {Object.keys(labelsForField).map((labelKey) => ( + + ))} + + + ); +}; diff --git a/datahub-web-react/src/app/search/SearchFilterLabel.tsx b/datahub-web-react/src/app/search/SearchFilterLabel.tsx index ccf4c6b40338f..bcd5249c360ec 100644 --- a/datahub-web-react/src/app/search/SearchFilterLabel.tsx +++ b/datahub-web-react/src/app/search/SearchFilterLabel.tsx @@ -26,6 +26,7 @@ import { formatNumber } from '../shared/formatNumber'; type Props = { aggregation: AggregationMetadata; field: string; + hideCount?: boolean; }; const PreviewImage = styled.img` @@ -39,16 +40,20 @@ const PreviewImage = styled.img` const MAX_COUNT_VAL = 10000; // SearchFilterLabel renders custom labels for entity, tag, term & data platform filters. All other filters use the default behavior. -export const SearchFilterLabel = ({ aggregation, field }: Props) => { +export const SearchFilterLabel = ({ aggregation, field, hideCount }: Props) => { const entityRegistry = useEntityRegistry(); - const countText = aggregation.count === MAX_COUNT_VAL ? '10000+' : aggregation.count; + const countText = hideCount + ? '' + : ` (${aggregation.count === MAX_COUNT_VAL ? '10k+' : formatNumber(aggregation.count)})`; + + if (!aggregation) return <>; if (field === ENTITY_FILTER_NAME) { const entityType = aggregation.value.toUpperCase() as EntityType; return ( - {entityType ? entityRegistry.getCollectionName(entityType) : aggregation.value} ( - {formatNumber(countText)}) + {entityType ? entityRegistry.getCollectionName(entityType) : aggregation.value} + {countText} ); } @@ -62,7 +67,7 @@ export const SearchFilterLabel = ({ aggregation, field }: Props) => { {truncatedDisplayName} - ({formatNumber(countText)}) + {countText} ); } @@ -82,7 +87,8 @@ export const SearchFilterLabel = ({ aggregation, field }: Props) => { marginRight: 8, }} /> - {displayName} ({countText}) + {displayName} + {countText} ); } @@ -96,7 +102,8 @@ export const SearchFilterLabel = ({ aggregation, field }: Props) => { {entityRegistry.getIcon(EntityType.CorpGroup, 16, IconStyleType.ACCENT)} - {truncatedDisplayName} ({formatNumber(countText)}) + {truncatedDisplayName} + {countText} ); } @@ -111,7 +118,7 @@ export const SearchFilterLabel = ({ aggregation, field }: Props) => { {truncatedDisplayName} - ({formatNumber(countText)}) + {countText} ); } @@ -126,7 +133,8 @@ export const SearchFilterLabel = ({ aggregation, field }: Props) => { )} - {truncatedDisplayName} ({formatNumber(countText)}) + {truncatedDisplayName} + {countText} ); @@ -138,7 +146,8 @@ export const SearchFilterLabel = ({ aggregation, field }: Props) => { const truncatedDisplayName = displayName.length > 25 ? `${displayName.slice(0, 25)}...` : displayName; return ( - {truncatedDisplayName} ({formatNumber(countText)}) + {truncatedDisplayName} + {countText} ); } @@ -153,7 +162,8 @@ export const SearchFilterLabel = ({ aggregation, field }: Props) => { )} - {truncatedDisplayName} ({formatNumber(countText)}) + {truncatedDisplayName} + {countText} ); @@ -165,7 +175,8 @@ export const SearchFilterLabel = ({ aggregation, field }: Props) => { const truncatedDomainName = displayName.length > 25 ? `${displayName.slice(0, 25)}...` : displayName; return ( - ({formatNumber(countText)}) + + {countText} ); } @@ -177,7 +188,8 @@ export const SearchFilterLabel = ({ aggregation, field }: Props) => { return ( - {truncatedDomainName} ({formatNumber(countText)}) + {truncatedDomainName} + {countText} ); @@ -188,7 +200,8 @@ export const SearchFilterLabel = ({ aggregation, field }: Props) => { } return ( <> - {aggregation.value} ({formatNumber(countText)}) + {aggregation.value} + {countText} ); }; diff --git a/datahub-web-react/src/app/search/utils/constants.ts b/datahub-web-react/src/app/search/utils/constants.ts index 053807cb51433..5d9f5d60c75a6 100644 --- a/datahub-web-react/src/app/search/utils/constants.ts +++ b/datahub-web-react/src/app/search/utils/constants.ts @@ -22,3 +22,39 @@ export const FILTERS_TO_TRUNCATE = [ PLATFORM_FILTER_NAME, ]; export const TRUNCATED_FILTER_LENGTH = 5; + +export const FIELD_TO_LABEL = { + owners: 'Owner', + tags: 'Tag', + domains: 'Domain', + platform: 'Platform', + fieldTags: 'Column Tag', + glossaryTerms: 'Term', + fieldGlossaryTerms: 'Column Term', + fieldPaths: 'Column Name', + description: 'Description', + fieldDescriptions: 'Column Description', + removed: 'Soft Deleted', + entity: 'Entity Type', + container: 'Container', + typeNames: 'Subtype', + origin: 'Environment', +}; + +export const FIELDS_WHO_USE_CONTAINS_OPERATOR = ['description', 'fieldDescriptions']; + +export const ADVANCED_SEARCH_ONLY_FILTERS = [ + 'fieldGlossaryTerms', + 'fieldTags', + 'fieldPaths', + 'description', + 'fieldDescriptions', + 'removed', +]; + +export const DEGREE_FILTER = 'degree'; + +export enum UnionType { + AND, + OR, +} From 6184d800d3e07099ce7b86aac67607e4ce146000 Mon Sep 17 00:00:00 2001 From: Gabe Lyons Date: Thu, 22 Sep 2022 16:00:05 -0700 Subject: [PATCH 3/6] adding advanced search filters component --- ...ncedSearchFilterOverallUnionTypeSelect.tsx | 43 +++++ .../src/app/search/AdvancedSearchFilters.tsx | 151 ++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 datahub-web-react/src/app/search/AdvancedSearchFilterOverallUnionTypeSelect.tsx create mode 100644 datahub-web-react/src/app/search/AdvancedSearchFilters.tsx diff --git a/datahub-web-react/src/app/search/AdvancedSearchFilterOverallUnionTypeSelect.tsx b/datahub-web-react/src/app/search/AdvancedSearchFilterOverallUnionTypeSelect.tsx new file mode 100644 index 0000000000000..b102cfead1bce --- /dev/null +++ b/datahub-web-react/src/app/search/AdvancedSearchFilterOverallUnionTypeSelect.tsx @@ -0,0 +1,43 @@ +import { Select } from 'antd'; +import React from 'react'; +import styled from 'styled-components/macro'; + +import { ANTD_GRAY } from '../entity/shared/constants'; +import { UnionType } from './utils/constants'; + +type Props = { + unionType: UnionType; + onUpdate: (newValue: UnionType) => void; +}; + +const { Option } = Select; + +const StyledSelect = styled(Select)` + border-radius: 5px; + background: ${ANTD_GRAY[4]}; + :hover { + background: ${ANTD_GRAY[4.5]}; + } +`; + +export const AdvancedSearchFilterOverallUnionTypeSelect = ({ unionType, onUpdate }: Props) => { + return ( + <> + { + if ((newValue as any) !== unionType) { + onUpdate(newValue as any); + } + }} + size="small" + dropdownMatchSelectWidth={false} + > + + + + + ); +}; diff --git a/datahub-web-react/src/app/search/AdvancedSearchFilters.tsx b/datahub-web-react/src/app/search/AdvancedSearchFilters.tsx new file mode 100644 index 0000000000000..db6a624d9dd6a --- /dev/null +++ b/datahub-web-react/src/app/search/AdvancedSearchFilters.tsx @@ -0,0 +1,151 @@ +import { Select } from 'antd'; +import * as React from 'react'; +import { useState } from 'react'; +import styled from 'styled-components'; +import { PlusOutlined } from '@ant-design/icons'; + +import { FacetFilterInput, FacetMetadata, SearchCondition } from '../../types.generated'; +import { ANTD_GRAY } from '../entity/shared/constants'; +import { AdvancedSearchFilter } from './AdvancedSearchFilter'; +import { AdvancedSearchFilterOverallUnionTypeSelect } from './AdvancedSearchFilterOverallUnionTypeSelect'; +import { AdvancedFilterSelectValueModal } from './AdvancedFilterSelectValueModal'; +import { FIELDS_WHO_USE_CONTAINS_OPERATOR, FIELD_TO_LABEL, UnionType } from './utils/constants'; + +export const SearchFilterWrapper = styled.div` + min-height: 100%; + overflow: auto; + margin-top: 6px; + margin-left: 12px; + margin-right: 12px; + + &::-webkit-scrollbar { + height: 12px; + width: 1px; + background: #f2f2f2; + } + &::-webkit-scrollbar-thumb { + background: #cccccc; + -webkit-border-radius: 1ex; + -webkit-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.75); + } +`; + +const AnyAllSection = styled.div` + padding: 6px; + color: ${ANTD_GRAY[8]}; +`; + +const EmptyStateSection = styled.div` + border-radius: 5px; + background-color: ${ANTD_GRAY[2]}; + padding: 22px; + margin-top: 10px; +`; + +const StyledPlus = styled(PlusOutlined)` + margin-right: 6px; +`; + +interface Props { + selectedFilters: Array; + facets: Array; + onFilterSelect: (newFilters: Array) => void; + onChangeUnionType: (unionType: UnionType) => void; + unionType?: UnionType; +} + +const { Option } = Select; + +export const AdvancedSearchFilters = ({ + unionType = UnionType.AND, + facets, + selectedFilters, + onFilterSelect, + onChangeUnionType, +}: Props) => { + const [filterField, setFilterField] = useState(null); + + const onFilterFieldSelect = (value) => { + setFilterField(value.value); + }; + + return ( + + + {selectedFilters?.length >= 2 && ( + + Show results that match{' '} + onChangeUnionType(newValue)} + /> + + )} + {selectedFilters.map((filter) => ( + facet.field === filter.field) || facets[0]} + filter={filter} + onClose={() => { + onFilterSelect(selectedFilters.filter((f) => f !== filter)); + }} + onUpdate={(newValue) => { + onFilterSelect( + selectedFilters.map((f) => { + if (f === filter) { + return newValue; + } + return f; + }), + ); + }} + /> + ))} + {filterField && ( + facet.field === filterField) || null} + onCloseModal={() => setFilterField(null)} + filterField={filterField} + onSelect={(values) => { + const newFilter: FacetFilterInput = { + field: filterField, + values: values as string[], + value: '', // TODO(Gabe): remove once we refactor the model + condition: + FIELDS_WHO_USE_CONTAINS_OPERATOR.indexOf(filterField) > -1 + ? SearchCondition.Contain + : SearchCondition.Equal, + }; + onFilterSelect([...selectedFilters, newFilter]); + }} + /> + )} + {selectedFilters?.length === 0 && No filters applied, add one above.} + + ); +}; From bb34f3c09d36a663bd09be67bbad9c3eb1e61c72 Mon Sep 17 00:00:00 2001 From: Gabe Lyons Date: Mon, 26 Sep 2022 15:30:53 -0700 Subject: [PATCH 4/6] removing console logs --- .../containers/profile/sidebar/Ownership/EditOwnersModal.tsx | 5 ----- datahub-web-react/src/app/shared/tags/AddTagsTermsModal.tsx | 1 - 2 files changed, 6 deletions(-) diff --git a/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/Ownership/EditOwnersModal.tsx b/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/Ownership/EditOwnersModal.tsx index 8c2ff9cd454cb..8b47ca5fd50ad 100644 --- a/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/Ownership/EditOwnersModal.tsx +++ b/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/Ownership/EditOwnersModal.tsx @@ -188,11 +188,6 @@ export const EditOwnersModal = ({ // When a owner search result is deselected, remove the Owner const onDeselectOwner = (selectedValue: { key: string; label: React.ReactNode; value: string }) => { - console.log({ - deselecting: 'deselecting', - selectedValue, - selectedOwners, - }); setInputValue(''); const newValues = selectedOwners.filter( (owner) => owner.label !== selectedValue.value && owner.value.ownerUrn !== selectedValue.value, diff --git a/datahub-web-react/src/app/shared/tags/AddTagsTermsModal.tsx b/datahub-web-react/src/app/shared/tags/AddTagsTermsModal.tsx index 2276eb406488e..5ac05c1da7569 100644 --- a/datahub-web-react/src/app/shared/tags/AddTagsTermsModal.tsx +++ b/datahub-web-react/src/app/shared/tags/AddTagsTermsModal.tsx @@ -91,7 +91,6 @@ export default function EditTagTermsModal({ defaultValues = [], onOkOverride, }: EditTagsModalProps) { - console.log({ defaultValues }); const entityRegistry = useEntityRegistry(); const [inputValue, setInputValue] = useState(''); const [showCreateModal, setShowCreateModal] = useState(false); From fc95022575841da35e45c3029ed09bebee4b5225 Mon Sep 17 00:00:00 2001 From: Gabe Lyons Date: Mon, 26 Sep 2022 15:47:38 -0700 Subject: [PATCH 5/6] improve readability --- .../search/AdvancedSearchAddFilterSelect.tsx | 50 +++++++++++++ .../src/app/search/AdvancedSearchFilters.tsx | 70 ++++++------------- 2 files changed, 72 insertions(+), 48 deletions(-) create mode 100644 datahub-web-react/src/app/search/AdvancedSearchAddFilterSelect.tsx diff --git a/datahub-web-react/src/app/search/AdvancedSearchAddFilterSelect.tsx b/datahub-web-react/src/app/search/AdvancedSearchAddFilterSelect.tsx new file mode 100644 index 0000000000000..d6eef72fae295 --- /dev/null +++ b/datahub-web-react/src/app/search/AdvancedSearchAddFilterSelect.tsx @@ -0,0 +1,50 @@ +import { Select } from 'antd'; +import * as React from 'react'; +import styled from 'styled-components'; +import { PlusOutlined } from '@ant-design/icons'; + +import { FacetFilterInput } from '../../types.generated'; +import { FIELD_TO_LABEL } from './utils/constants'; + +const StyledPlus = styled(PlusOutlined)` + margin-right: 6px; +`; + +interface Props { + selectedFilters: Array; + onFilterFieldSelect: (value) => void; +} + +const { Option } = Select; + +export const AdvancedSearchAddFilterSelect = ({ selectedFilters, onFilterFieldSelect }: Props) => { + return ( + + ); +}; diff --git a/datahub-web-react/src/app/search/AdvancedSearchFilters.tsx b/datahub-web-react/src/app/search/AdvancedSearchFilters.tsx index db6a624d9dd6a..fa2dee215a726 100644 --- a/datahub-web-react/src/app/search/AdvancedSearchFilters.tsx +++ b/datahub-web-react/src/app/search/AdvancedSearchFilters.tsx @@ -1,15 +1,14 @@ -import { Select } from 'antd'; import * as React from 'react'; import { useState } from 'react'; import styled from 'styled-components'; -import { PlusOutlined } from '@ant-design/icons'; import { FacetFilterInput, FacetMetadata, SearchCondition } from '../../types.generated'; import { ANTD_GRAY } from '../entity/shared/constants'; import { AdvancedSearchFilter } from './AdvancedSearchFilter'; import { AdvancedSearchFilterOverallUnionTypeSelect } from './AdvancedSearchFilterOverallUnionTypeSelect'; import { AdvancedFilterSelectValueModal } from './AdvancedFilterSelectValueModal'; -import { FIELDS_WHO_USE_CONTAINS_OPERATOR, FIELD_TO_LABEL, UnionType } from './utils/constants'; +import { FIELDS_THAT_USE_CONTAINS_OPERATOR, UnionType } from './utils/constants'; +import { AdvancedSearchAddFilterSelect } from './AdvancedSearchAddFilterSelect'; export const SearchFilterWrapper = styled.div` min-height: 100%; @@ -42,10 +41,6 @@ const EmptyStateSection = styled.div` margin-top: 10px; `; -const StyledPlus = styled(PlusOutlined)` - margin-right: 6px; -`; - interface Props { selectedFilters: Array; facets: Array; @@ -54,8 +49,6 @@ interface Props { unionType?: UnionType; } -const { Option } = Select; - export const AdvancedSearchFilters = ({ unionType = UnionType.AND, facets, @@ -69,35 +62,27 @@ export const AdvancedSearchFilters = ({ setFilterField(value.value); }; + const onSelectValueFromModal = (values) => { + if (!filterField) return; + + const newFilter: FacetFilterInput = { + field: filterField, + values: values as string[], + value: '', // TODO(Gabe): remove once we refactor the model + condition: + FIELDS_THAT_USE_CONTAINS_OPERATOR.indexOf(filterField) > -1 + ? SearchCondition.Contain + : SearchCondition.Equal, + }; + onFilterSelect([...selectedFilters, newFilter]); + }; + return ( - + {selectedFilters?.length >= 2 && ( Show results that match{' '} @@ -131,18 +116,7 @@ export const AdvancedSearchFilters = ({ facet={facets.find((facet) => facet.field === filterField) || null} onCloseModal={() => setFilterField(null)} filterField={filterField} - onSelect={(values) => { - const newFilter: FacetFilterInput = { - field: filterField, - values: values as string[], - value: '', // TODO(Gabe): remove once we refactor the model - condition: - FIELDS_WHO_USE_CONTAINS_OPERATOR.indexOf(filterField) > -1 - ? SearchCondition.Contain - : SearchCondition.Equal, - }; - onFilterSelect([...selectedFilters, newFilter]); - }} + onSelect={onSelectValueFromModal} /> )} {selectedFilters?.length === 0 && No filters applied, add one above.} From 1cb0d204025607d9f6ae63bbf5bf8fc11fb4ee66 Mon Sep 17 00:00:00 2001 From: Gabe Lyons Date: Thu, 29 Sep 2022 15:58:29 -0700 Subject: [PATCH 6/6] responding to comments --- .../src/app/search/AdvancedSearchAddFilterSelect.tsx | 1 + .../search/AdvancedSearchFilterOverallUnionTypeSelect.tsx | 1 + datahub-web-react/src/app/search/AdvancedSearchFilters.tsx | 7 +++---- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/datahub-web-react/src/app/search/AdvancedSearchAddFilterSelect.tsx b/datahub-web-react/src/app/search/AdvancedSearchAddFilterSelect.tsx index d6eef72fae295..5130ecc3b628b 100644 --- a/datahub-web-react/src/app/search/AdvancedSearchAddFilterSelect.tsx +++ b/datahub-web-react/src/app/search/AdvancedSearchAddFilterSelect.tsx @@ -39,6 +39,7 @@ export const AdvancedSearchAddFilterSelect = ({ selectedFilters, onFilterFieldSe .sort((a, b) => FIELD_TO_LABEL[a].localeCompare(FIELD_TO_LABEL[b])) .map((key) => (