From bf7da0a853e8459e71181883cc7cd57e69db1dc8 Mon Sep 17 00:00:00 2001 From: Ankit keshari <86347578+Ankit-Keshari-Vituity@users.noreply.github.com> Date: Tue, 28 Jun 2022 22:15:25 +0530 Subject: [PATCH] feat(ui): Selector recommendations in Owner, Tag and Domain Modal (#5197) --- .../entity/group/GroupOwnerSideBarSection.tsx | 21 +- .../profile/sidebar/Domain/SetDomainModal.tsx | 191 ++++++++------- .../sidebar/Domain/SidebarDomainSection.tsx | 15 +- .../sidebar/Ownership/AddOwnersModal.tsx | 226 ++++++++---------- .../sidebar/Ownership/SidebarOwnerSection.tsx | 23 +- .../src/app/shared/DomainLabel.tsx | 29 +++ .../src/app/shared/OwnerLabel.tsx | 34 +++ .../src/app/shared/TagStyleEntity.tsx | 21 +- .../src/app/shared/recommendation.tsx | 18 ++ .../src/app/shared/tags/AddTagsTermsModal.tsx | 96 +++++--- 10 files changed, 394 insertions(+), 280 deletions(-) create mode 100644 datahub-web-react/src/app/shared/DomainLabel.tsx create mode 100644 datahub-web-react/src/app/shared/OwnerLabel.tsx create mode 100644 datahub-web-react/src/app/shared/recommendation.tsx diff --git a/datahub-web-react/src/app/entity/group/GroupOwnerSideBarSection.tsx b/datahub-web-react/src/app/entity/group/GroupOwnerSideBarSection.tsx index 1708d6f5aa62b..2cf84bc375c0c 100644 --- a/datahub-web-react/src/app/entity/group/GroupOwnerSideBarSection.tsx +++ b/datahub-web-react/src/app/entity/group/GroupOwnerSideBarSection.tsx @@ -50,16 +50,17 @@ export default function GroupOwnerSideBarSection({ urn, ownership, refetch }: Pr )} - { - setShowAddModal(false); - }} - /> + {showAddModal && ( + { + setShowAddModal(false); + }} + /> + )} ); } 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 d0db0fe5be440..afd634326cd8b 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 @@ -1,82 +1,89 @@ -import { Button, Form, message, Modal, Select, Tag } from 'antd'; import React, { useRef, useState } from 'react'; +import { Button, Form, message, Modal, Select, Tag } from 'antd'; import styled from 'styled-components'; -import { Link } from 'react-router-dom'; + import { useGetSearchResultsLazyQuery } from '../../../../../../../graphql/search.generated'; -import { EntityType, SearchResult } from '../../../../../../../types.generated'; +import { Entity, EntityType } from '../../../../../../../types.generated'; import { useSetDomainMutation } from '../../../../../../../graphql/mutations.generated'; import { useEntityRegistry } from '../../../../../../useEntityRegistry'; import { useEntityData } from '../../../../EntityContext'; import { useEnterKeyListener } from '../../../../../../shared/useEnterKeyListener'; +import { useGetRecommendations } from '../../../../../../shared/recommendation'; +import { DomainLabel } from '../../../../../../shared/DomainLabel'; type Props = { - visible: boolean; - onClose: () => void; + onCloseModal: () => void; refetch?: () => Promise; }; -const SearchResultContainer = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px; -`; - -const SearchResultContent = styled.div` - display: flex; - justify-content: start; - align-items: center; -`; - -const SearchResultDisplayName = styled.div` - margin-left: 12px; -`; - type SelectedDomain = { displayName: string; type: EntityType; urn: string; }; -export const SetDomainModal = ({ visible, onClose, refetch }: Props) => { +const StyleTag = styled(Tag)` + padding: 0px 7px; + margin-right: 3px; + display: flex; + justify-content: start; + align-items: center; +`; + +export const SetDomainModal = ({ onCloseModal, refetch }: Props) => { const entityRegistry = useEntityRegistry(); const { urn } = useEntityData(); + const [inputValue, setInputValue] = useState(''); const [selectedDomain, setSelectedDomain] = useState(undefined); const [domainSearch, { data: domainSearchData }] = useGetSearchResultsLazyQuery(); - const domainSearchResults = domainSearchData?.search?.searchResults || []; + const domainSearchResults = + domainSearchData?.search?.searchResults?.map((searchResult) => searchResult.entity) || []; const [setDomainMutation] = useSetDomainMutation(); - + const [recommendedData] = useGetRecommendations([EntityType.Domain]); const inputEl = useRef(null); - const onOk = async () => { - if (!selectedDomain) { - return; - } - try { - await setDomainMutation({ + const onModalClose = () => { + setInputValue(''); + setSelectedDomain(undefined); + onCloseModal(); + }; + + const handleSearch = (text: string) => { + if (text.length > 2) { + domainSearch({ variables: { - entityUrn: urn, - domainUrn: selectedDomain.urn, + input: { + type: EntityType.Domain, + query: text, + start: 0, + count: 5, + }, }, }); - message.success({ content: 'Updated Domain!', duration: 2 }); - } catch (e: unknown) { - message.destroy(); - if (e instanceof Error) { - message.error({ content: `Failed to set Domain: \n ${e.message || ''}`, duration: 3 }); - } } - setSelectedDomain(undefined); - refetch?.(); - onClose(); }; + // Renders a search result in the select dropdown. + const renderSearchResult = (entity: Entity) => { + const displayName = entityRegistry.getDisplayName(entity.type, entity); + return ( + + + + ); + }; + + const domainResult = !inputValue || inputValue.length === 0 ? recommendedData : domainSearchResults; + + const domainSearchOptions = domainResult?.map((result) => { + return renderSearchResult(result); + }); + const onSelectDomain = (newUrn: string) => { if (inputEl && inputEl.current) { (inputEl.current as any).blur(); } - const filteredDomains = - domainSearchResults?.filter((result) => result.entity.urn === newUrn).map((result) => result.entity) || []; + const filteredDomains = domainResult?.filter((entity) => entity.urn === newUrn).map((entity) => entity) || []; if (filteredDomains.length) { const domain = filteredDomains[0]; setSelectedDomain({ @@ -87,56 +94,67 @@ export const SetDomainModal = ({ visible, onClose, refetch }: Props) => { } }; - const handleSearch = (text: string) => { - if (text.length > 2) { - domainSearch({ + const onDeselectDomain = () => { + setInputValue(''); + setSelectedDomain(undefined); + }; + + const onOk = async () => { + if (!selectedDomain) { + return; + } + try { + await setDomainMutation({ variables: { - input: { - type: EntityType.Domain, - query: text, - start: 0, - count: 5, - }, + entityUrn: urn, + domainUrn: selectedDomain.urn, }, }); + message.success({ content: 'Updated Domain!', duration: 2 }); + } catch (e: unknown) { + message.destroy(); + if (e instanceof Error) { + message.error({ content: `Failed to set Domain: \n ${e.message || ''}`, duration: 3 }); + } } + setSelectedDomain(undefined); + refetch?.(); + onModalClose(); }; + const selectValue = (selectedDomain && [selectedDomain?.displayName]) || undefined; + // Handle the Enter press useEnterKeyListener({ querySelectorToExecuteClick: '#setDomainButton', }); - const renderSearchResult = (result: SearchResult) => { - const displayName = entityRegistry.getDisplayName(result.entity.type, result.entity); + const tagRender = (props) => { + // eslint-disable-next-line react/prop-types + const { label, closable, onClose } = props; + const onPreventMouseDown = (event) => { + event.preventDefault(); + event.stopPropagation(); + }; return ( - - - -
{displayName}
-
-
- `/${entityRegistry.getPathName(result.entity.type)}/${result.entity.urn}`} - > - View - {' '} -
+ + {label} + ); }; - const selectValue = (selectedDomain && [selectedDomain?.displayName]) || []; + function handleBlur() { + setInputValue(''); + } return ( - - { - setShowAddModal(false); - }} - /> + {showAddModal && ( + { + setShowAddModal(false); + }} + /> + )} ); }; diff --git a/datahub-web-react/src/app/shared/DomainLabel.tsx b/datahub-web-react/src/app/shared/DomainLabel.tsx new file mode 100644 index 0000000000000..40208026d4369 --- /dev/null +++ b/datahub-web-react/src/app/shared/DomainLabel.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import styled from 'styled-components'; + +const DomainContainerWrapper = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px; +`; + +const DomainContentWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; +`; + +type Props = { + name: string; +}; + +export const DomainLabel = ({ name }: Props) => { + return ( + + +
{name}
+
+
+ ); +}; diff --git a/datahub-web-react/src/app/shared/OwnerLabel.tsx b/datahub-web-react/src/app/shared/OwnerLabel.tsx new file mode 100644 index 0000000000000..de3c03dea2ba4 --- /dev/null +++ b/datahub-web-react/src/app/shared/OwnerLabel.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import styled from 'styled-components'; +import { EntityType } from '../../types.generated'; +import { CustomAvatar } from './avatar'; + +const OwnerContainerWrapper = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding: 2px; +`; + +const OwnerContentWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; +`; + +type Props = { + name: string; + avatarUrl: string | undefined; + type: EntityType; +}; + +export const OwnerLabel = ({ name, avatarUrl, type }: Props) => { + return ( + + + +
{name}
+
+
+ ); +}; diff --git a/datahub-web-react/src/app/shared/TagStyleEntity.tsx b/datahub-web-react/src/app/shared/TagStyleEntity.tsx index 23460ccef8dc9..7a246d9ed839e 100644 --- a/datahub-web-react/src/app/shared/TagStyleEntity.tsx +++ b/datahub-web-react/src/app/shared/TagStyleEntity.tsx @@ -414,16 +414,17 @@ export default function TagStyleEntity({ urn, useGetSearchResults = useWrappedSe
- { - setShowAddModal(false); - }} - urn={urn} - type={EntityType.Tag} - /> + {showAddModal && ( + { + setShowAddModal(false); + }} + urn={urn} + type={EntityType.Tag} + /> + )}
diff --git a/datahub-web-react/src/app/shared/recommendation.tsx b/datahub-web-react/src/app/shared/recommendation.tsx new file mode 100644 index 0000000000000..19a3087796152 --- /dev/null +++ b/datahub-web-react/src/app/shared/recommendation.tsx @@ -0,0 +1,18 @@ +import { useGetSearchResultsForMultipleQuery } from '../../graphql/search.generated'; +import { EntityType } from '../../types.generated'; + +export const useGetRecommendations = (types: Array) => { + const { data } = useGetSearchResultsForMultipleQuery({ + variables: { + input: { + types, + query: '*', + start: 0, + count: 5, + }, + }, + }); + + const recommendedData = data?.searchAcrossEntities?.searchResults?.map((searchResult) => searchResult.entity) || []; + return [recommendedData]; +}; diff --git a/datahub-web-react/src/app/shared/tags/AddTagsTermsModal.tsx b/datahub-web-react/src/app/shared/tags/AddTagsTermsModal.tsx index 00479f3a0514e..9101ad34029d0 100644 --- a/datahub-web-react/src/app/shared/tags/AddTagsTermsModal.tsx +++ b/datahub-web-react/src/app/shared/tags/AddTagsTermsModal.tsx @@ -1,11 +1,10 @@ -import React, { useState } from 'react'; +import React, { useRef, useState } from 'react'; import { message, Button, Modal, Select, Typography, Tag as CustomTag } from 'antd'; import styled from 'styled-components'; import { useGetSearchResultsLazyQuery } from '../../../graphql/search.generated'; -import { EntityType, SubResourceType, SearchResult, Tag } from '../../../types.generated'; +import { EntityType, SubResourceType, Tag, Entity } from '../../../types.generated'; import CreateTagModal from './CreateTagModal'; -import { useEntityRegistry } from '../../useEntityRegistry'; import { useAddTagsMutation, useAddTermsMutation } from '../../../graphql/mutations.generated'; import analytics, { EventType, EntityActionType } from '../../analytics'; import { useEnterKeyListener } from '../useEnterKeyListener'; @@ -13,6 +12,8 @@ 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'; type AddTagsModalProps = { visible: boolean; @@ -27,6 +28,17 @@ const TagSelect = styled(Select)` width: 480px; `; +const StyleTag = styled(CustomTag)` + margin-right: 3px; + display: flex; + justify-content: start; + align-items: center; + white-space: nowrap; + opacity: 1; + color: #434343; + line-height: 16px; +`; + export const BrowserWrapper = styled.div<{ isHidden: boolean }>` background-color: white; border-radius: 5px; @@ -61,13 +73,16 @@ export default function AddTagsTermsModal({ const [disableAdd, setDisableAdd] = useState(false); const [urns, setUrns] = useState([]); const [selectedTerms, setSelectedTerms] = useState([]); + const [selectedTags, setSelectedTags] = useState([]); const [isFocusedOnInput, setIsFocusedOnInput] = useState(false); const [addTagsMutation] = useAddTagsMutation(); const [addTermsMutation] = useAddTermsMutation(); const [tagTermSearch, { data: tagTermSearchData }] = useGetSearchResultsLazyQuery(); - const tagSearchResults = tagTermSearchData?.search?.searchResults || []; + const tagSearchResults = tagTermSearchData?.search?.searchResults?.map((searchResult) => searchResult.entity) || []; + const [recommendedData] = useGetRecommendations([EntityType.Tag]); + const inputEl = useRef(null); const handleSearch = (text: string) => { if (text.length > 0) { @@ -84,39 +99,40 @@ export default function AddTagsTermsModal({ } }; - const renderSearchResult = (result: SearchResult) => { + const renderSearchResult = (entity: Entity) => { const displayName = - result.entity.type === EntityType.Tag - ? (result.entity as Tag).name - : entityRegistry.getDisplayName(result.entity.type, result.entity); + entity.type === EntityType.Tag ? (entity as Tag).name : entityRegistry.getDisplayName(entity.type, entity); const tagOrTermComponent = - result.entity.type === EntityType.Tag ? ( + entity.type === EntityType.Tag ? ( ) : ( ); return ( - + {tagOrTermComponent} ); }; - const tagSearchOptions = tagSearchResults.map((result) => { + const tagResult = + (!inputValue || inputValue.length === 0) && type === EntityType.Tag ? recommendedData : tagSearchResults; + + const tagSearchOptions = tagResult?.map((result) => { return renderSearchResult(result); }); - const inputExistsInTagSearch = tagSearchResults.some((result: SearchResult) => { - const displayName = entityRegistry.getDisplayName(result.entity.type, result.entity); + const inputExistsInTagSearch = tagSearchResults.some((entity: Entity) => { + const displayName = entityRegistry.getDisplayName(entity.type, entity); return displayName.toLowerCase() === inputValue.toLowerCase(); }); if (!inputExistsInTagSearch && inputValue.length > 0 && type === EntityType.Tag && urns.length === 0) { - tagSearchOptions.push( + tagSearchOptions?.push( Create {inputValue} , @@ -125,32 +141,20 @@ export default function AddTagsTermsModal({ const tagRender = (props) => { // eslint-disable-next-line react/prop-types - const { label, closable, onClose, value } = props; + const { closable, onClose, value } = props; const onPreventMouseDown = (event) => { event.preventDefault(); event.stopPropagation(); }; const selectedItem = - type === EntityType.GlossaryTerm ? selectedTerms.find((term) => term.urn === value).component : label; + type === EntityType.GlossaryTerm + ? selectedTerms.find((term) => term.urn === value).component + : selectedTags.find((term) => term.urn === value).component; return ( - + {selectedItem} - + ); }; @@ -179,9 +183,26 @@ export default function AddTagsTermsModal({ return; } const newUrns = [...(urns || []), urn]; + const selectedSearchOption = tagSearchOptions?.find((option) => option.props.value === urn); + const selectedTagOption = tagResult?.find((tag) => tag.urn === urn); setUrns(newUrns); - const selectedSearchOption = tagSearchOptions.find((option) => option.props.value === urn); setSelectedTerms([...selectedTerms, { urn, component: }]); + setSelectedTags([ + ...selectedTags, + { + urn, + component: ( + + ), + }, + ]); + if (inputEl && inputEl.current) { + (inputEl.current as any).blur(); + } }; // When a Tag or term search result is deselected, remove the urn from the Owners @@ -191,6 +212,7 @@ export default function AddTagsTermsModal({ setInputValue(''); setIsFocusedOnInput(true); setSelectedTerms(selectedTerms.filter((term) => term.urn !== urn)); + setSelectedTags(selectedTags.filter((term) => term.urn !== urn)); }; // Function to handle the modal action's @@ -313,7 +335,9 @@ export default function AddTagsTermsModal({ setIsFocusedOnInput(false)}> setIsFocusedOnInput(true)} onBlur={handleBlur} - dropdownStyle={isShowingGlossaryBrowser || !inputValue ? { display: 'none' } : {}} + dropdownStyle={isShowingGlossaryBrowser ? { display: 'none' } : {}} > {tagSearchOptions}