mirror of
https://github.com/datahub-project/datahub.git
synced 2025-11-02 19:58:59 +00:00
feat(ui): Selector recommendations in Owner, Tag and Domain Modal (#5197)
This commit is contained in:
parent
b76005d640
commit
bf7da0a853
@ -50,16 +50,17 @@ export default function GroupOwnerSideBarSection({ urn, ownership, refetch }: Pr
|
||||
</AddOwnerButton>
|
||||
)}
|
||||
</SectionWrapper>
|
||||
<AddOwnersModal
|
||||
urn={urn}
|
||||
hideOwnerType
|
||||
type={EntityType.CorpGroup}
|
||||
visible={showAddModal}
|
||||
refetch={refetch}
|
||||
onCloseModal={() => {
|
||||
setShowAddModal(false);
|
||||
}}
|
||||
/>
|
||||
{showAddModal && (
|
||||
<AddOwnersModal
|
||||
urn={urn}
|
||||
hideOwnerType
|
||||
type={EntityType.CorpGroup}
|
||||
refetch={refetch}
|
||||
onCloseModal={() => {
|
||||
setShowAddModal(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,53 +1,104 @@
|
||||
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<any>;
|
||||
};
|
||||
|
||||
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<SelectedDomain | undefined>(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 onModalClose = () => {
|
||||
setInputValue('');
|
||||
setSelectedDomain(undefined);
|
||||
onCloseModal();
|
||||
};
|
||||
|
||||
const handleSearch = (text: string) => {
|
||||
if (text.length > 2) {
|
||||
domainSearch({
|
||||
variables: {
|
||||
input: {
|
||||
type: EntityType.Domain,
|
||||
query: text,
|
||||
start: 0,
|
||||
count: 5,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Renders a search result in the select dropdown.
|
||||
const renderSearchResult = (entity: Entity) => {
|
||||
const displayName = entityRegistry.getDisplayName(entity.type, entity);
|
||||
return (
|
||||
<Select.Option value={entity.urn} key={entity.urn}>
|
||||
<DomainLabel name={displayName} />
|
||||
</Select.Option>
|
||||
);
|
||||
};
|
||||
|
||||
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 = domainResult?.filter((entity) => entity.urn === newUrn).map((entity) => entity) || [];
|
||||
if (filteredDomains.length) {
|
||||
const domain = filteredDomains[0];
|
||||
setSelectedDomain({
|
||||
displayName: entityRegistry.getDisplayName(EntityType.Domain, domain),
|
||||
type: EntityType.Domain,
|
||||
urn: newUrn,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onDeselectDomain = () => {
|
||||
setInputValue('');
|
||||
setSelectedDomain(undefined);
|
||||
};
|
||||
|
||||
const onOk = async () => {
|
||||
if (!selectedDomain) {
|
||||
return;
|
||||
@ -68,75 +119,42 @@ export const SetDomainModal = ({ visible, onClose, refetch }: Props) => {
|
||||
}
|
||||
setSelectedDomain(undefined);
|
||||
refetch?.();
|
||||
onClose();
|
||||
onModalClose();
|
||||
};
|
||||
|
||||
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) || [];
|
||||
if (filteredDomains.length) {
|
||||
const domain = filteredDomains[0];
|
||||
setSelectedDomain({
|
||||
displayName: entityRegistry.getDisplayName(EntityType.Domain, domain),
|
||||
type: EntityType.Domain,
|
||||
urn: newUrn,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = (text: string) => {
|
||||
if (text.length > 2) {
|
||||
domainSearch({
|
||||
variables: {
|
||||
input: {
|
||||
type: EntityType.Domain,
|
||||
query: text,
|
||||
start: 0,
|
||||
count: 5,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
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 (
|
||||
<SearchResultContainer>
|
||||
<SearchResultContent>
|
||||
<SearchResultDisplayName>
|
||||
<div>{displayName}</div>
|
||||
</SearchResultDisplayName>
|
||||
</SearchResultContent>
|
||||
<Link
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
to={() => `/${entityRegistry.getPathName(result.entity.type)}/${result.entity.urn}`}
|
||||
>
|
||||
View
|
||||
</Link>{' '}
|
||||
</SearchResultContainer>
|
||||
<StyleTag onMouseDown={onPreventMouseDown} closable={closable} onClose={onClose}>
|
||||
{label}
|
||||
</StyleTag>
|
||||
);
|
||||
};
|
||||
|
||||
const selectValue = (selectedDomain && [selectedDomain?.displayName]) || [];
|
||||
function handleBlur() {
|
||||
setInputValue('');
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Set Domain"
|
||||
visible={visible}
|
||||
onCancel={onClose}
|
||||
visible
|
||||
onCancel={onModalClose}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onClose} type="text">
|
||||
<Button onClick={onModalClose} type="text">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button id="setDomainButton" disabled={selectedDomain === undefined} onClick={onOk}>
|
||||
@ -149,21 +167,26 @@ export const SetDomainModal = ({ visible, onClose, refetch }: Props) => {
|
||||
<Form.Item>
|
||||
<Select
|
||||
autoFocus
|
||||
value={selectValue}
|
||||
defaultOpen
|
||||
filterOption={false}
|
||||
showSearch
|
||||
mode="multiple"
|
||||
ref={inputEl}
|
||||
defaultActiveFirstOption={false}
|
||||
placeholder="Search for Domains..."
|
||||
onSelect={(domainUrn: any) => onSelectDomain(domainUrn)}
|
||||
onDeselect={() => setSelectedDomain(undefined)}
|
||||
onSearch={handleSearch}
|
||||
filterOption={false}
|
||||
tagRender={(tagProps) => <Tag>{tagProps.value}</Tag>}
|
||||
onDeselect={onDeselectDomain}
|
||||
onSearch={(value: string) => {
|
||||
// eslint-disable-next-line react/prop-types
|
||||
handleSearch(value.trim());
|
||||
// eslint-disable-next-line react/prop-types
|
||||
setInputValue(value.trim());
|
||||
}}
|
||||
ref={inputEl}
|
||||
value={selectValue}
|
||||
tagRender={tagRender}
|
||||
onBlur={handleBlur}
|
||||
>
|
||||
{domainSearchResults.map((result) => {
|
||||
return (
|
||||
<Select.Option value={result.entity.urn}>{renderSearchResult(result)}</Select.Option>
|
||||
);
|
||||
})}
|
||||
{domainSearchOptions}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
@ -73,13 +73,14 @@ export const SidebarDomainSection = () => {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<SetDomainModal
|
||||
visible={showModal}
|
||||
refetch={refetch}
|
||||
onClose={() => {
|
||||
setShowModal(false);
|
||||
}}
|
||||
/>
|
||||
{showModal && (
|
||||
<SetDomainModal
|
||||
refetch={refetch}
|
||||
onCloseModal={() => {
|
||||
setShowModal(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,33 +1,15 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Button, Form, message, Modal, Select, Tag, Typography } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
CorpUser,
|
||||
EntityType,
|
||||
OwnerEntityType,
|
||||
OwnershipType,
|
||||
SearchResult,
|
||||
} from '../../../../../../../types.generated';
|
||||
import { CorpUser, Entity, EntityType, OwnerEntityType, OwnershipType } from '../../../../../../../types.generated';
|
||||
import { useEntityRegistry } from '../../../../../../useEntityRegistry';
|
||||
import { CustomAvatar } from '../../../../../../shared/avatar';
|
||||
import analytics, { EventType, EntityActionType } from '../../../../../../analytics';
|
||||
import { OWNERSHIP_DISPLAY_TYPES } from './ownershipUtils';
|
||||
import { useAddOwnersMutation } from '../../../../../../../graphql/mutations.generated';
|
||||
import { useGetSearchResultsLazyQuery } from '../../../../../../../graphql/search.generated';
|
||||
|
||||
const SearchResultContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 2px;
|
||||
`;
|
||||
|
||||
const SearchResultContent = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
import { useGetRecommendations } from '../../../../../../shared/recommendation';
|
||||
import { OwnerLabel } from '../../../../../../shared/OwnerLabel';
|
||||
|
||||
const SelectInput = styled(Select)`
|
||||
> .ant-select-selector {
|
||||
@ -35,10 +17,17 @@ const SelectInput = styled(Select)`
|
||||
}
|
||||
`;
|
||||
|
||||
const StyleTag = styled(Tag)`
|
||||
padding: 0px 7px 0px 0px;
|
||||
margin-right: 3px;
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
urn: string;
|
||||
type: EntityType;
|
||||
visible: boolean;
|
||||
defaultOwnerType?: OwnershipType;
|
||||
hideOwnerType?: boolean | undefined;
|
||||
onCloseModal: () => void;
|
||||
@ -51,16 +40,9 @@ type SelectedOwner = {
|
||||
value;
|
||||
};
|
||||
|
||||
export const AddOwnersModal = ({
|
||||
urn,
|
||||
type,
|
||||
visible,
|
||||
hideOwnerType,
|
||||
defaultOwnerType,
|
||||
onCloseModal,
|
||||
refetch,
|
||||
}: Props) => {
|
||||
export const AddOwnersModal = ({ urn, type, hideOwnerType, defaultOwnerType, onCloseModal, refetch }: Props) => {
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [addOwnersMutation] = useAddOwnersMutation();
|
||||
const ownershipTypes = OWNERSHIP_DISPLAY_TYPES;
|
||||
const [selectedOwners, setSelectedOwners] = useState<SelectedOwner[]>([]);
|
||||
@ -69,12 +51,11 @@ export const AddOwnersModal = ({
|
||||
// User and group dropdown search results!
|
||||
const [userSearch, { data: userSearchData }] = useGetSearchResultsLazyQuery();
|
||||
const [groupSearch, { data: groupSearchData }] = useGetSearchResultsLazyQuery();
|
||||
const userSearchResults = userSearchData?.search?.searchResults || [];
|
||||
const groupSearchResults = groupSearchData?.search?.searchResults || [];
|
||||
const userSearchResults = userSearchData?.search?.searchResults?.map((searchResult) => searchResult.entity) || [];
|
||||
const groupSearchResults = groupSearchData?.search?.searchResults?.map((searchResult) => searchResult.entity) || [];
|
||||
const combinedSearchResults = [...userSearchResults, ...groupSearchResults];
|
||||
|
||||
// Add owners Form
|
||||
const [form] = Form.useForm();
|
||||
const [recommendedData] = useGetRecommendations([EntityType.CorpGroup, EntityType.CorpUser]);
|
||||
const inputEl = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ownershipTypes) {
|
||||
@ -82,43 +63,6 @@ export const AddOwnersModal = ({
|
||||
}
|
||||
}, [ownershipTypes]);
|
||||
|
||||
/**
|
||||
* When a owner search result is selected, add the new owner to the selectedOwners
|
||||
* value: {ownerUrn: string, ownerEntityType: EntityType}
|
||||
*/
|
||||
const onSelectOwner = (selectedValue: { key: string; label: React.ReactNode; value: string }) => {
|
||||
const filteredActors = combinedSearchResults
|
||||
.filter((result) => result.entity.urn === selectedValue.value)
|
||||
.map((result) => result.entity);
|
||||
if (filteredActors.length) {
|
||||
const actor = filteredActors[0];
|
||||
const ownerEntityType =
|
||||
actor && actor.type === EntityType.CorpGroup ? OwnerEntityType.CorpGroup : OwnerEntityType.CorpUser;
|
||||
const newValues = [
|
||||
...selectedOwners,
|
||||
{
|
||||
label: selectedValue.value,
|
||||
value: {
|
||||
ownerUrn: selectedValue.value,
|
||||
ownerEntityType,
|
||||
},
|
||||
},
|
||||
];
|
||||
setSelectedOwners(newValues);
|
||||
}
|
||||
};
|
||||
|
||||
// When a owner search result is deselected, remove the Owner
|
||||
const onDeselectOwner = (selectedValue: { key: string; label: React.ReactNode; value: string }) => {
|
||||
const newValues = selectedOwners.filter((owner) => owner.label !== selectedValue.value);
|
||||
setSelectedOwners(newValues);
|
||||
};
|
||||
|
||||
// When a owner type is selected, set the type as selected type.
|
||||
const onSelectOwnerType = (newType: OwnershipType) => {
|
||||
setSelectedOwnerType(newType);
|
||||
};
|
||||
|
||||
// Invokes the search API as the owner types
|
||||
const handleSearch = (entityType: EntityType, text: string, searchQuery: any) => {
|
||||
if (text.length > 2) {
|
||||
@ -142,34 +86,87 @@ export const AddOwnersModal = ({
|
||||
};
|
||||
|
||||
// Renders a search result in the select dropdown.
|
||||
const renderSearchResult = (result: SearchResult) => {
|
||||
const renderSearchResult = (entity: Entity) => {
|
||||
const avatarUrl =
|
||||
result.entity.type === EntityType.CorpUser
|
||||
? (result.entity as CorpUser).editableProperties?.pictureLink || undefined
|
||||
entity.type === EntityType.CorpUser
|
||||
? (entity as CorpUser).editableProperties?.pictureLink || undefined
|
||||
: undefined;
|
||||
const displayName = entityRegistry.getDisplayName(result.entity.type, result.entity);
|
||||
const displayName = entityRegistry.getDisplayName(entity.type, entity);
|
||||
return (
|
||||
<SearchResultContainer>
|
||||
<SearchResultContent>
|
||||
<CustomAvatar
|
||||
size={24}
|
||||
name={displayName}
|
||||
photoUrl={avatarUrl}
|
||||
isGroup={result.entity.type === EntityType.CorpGroup}
|
||||
/>
|
||||
<div>{displayName}</div>
|
||||
</SearchResultContent>
|
||||
</SearchResultContainer>
|
||||
<Select.Option value={entity.urn} key={entity.urn}>
|
||||
<OwnerLabel name={displayName} avatarUrl={avatarUrl} type={entity.type} />
|
||||
</Select.Option>
|
||||
);
|
||||
};
|
||||
|
||||
const ownerResult = !inputValue || inputValue.length === 0 ? recommendedData : combinedSearchResults;
|
||||
|
||||
const ownerSearchOptions = ownerResult?.map((result) => {
|
||||
return renderSearchResult(result);
|
||||
});
|
||||
|
||||
const onModalClose = () => {
|
||||
setInputValue('');
|
||||
setSelectedOwners([]);
|
||||
setSelectedOwnerType(defaultOwnerType || OwnershipType.None);
|
||||
form.resetFields();
|
||||
onCloseModal();
|
||||
};
|
||||
|
||||
/**
|
||||
* When a owner search result is selected, add the new owner to the selectedOwners
|
||||
* value: {ownerUrn: string, ownerEntityType: EntityType}
|
||||
*/
|
||||
const onSelectOwner = (selectedValue: { key: string; label: React.ReactNode; value: string }) => {
|
||||
if (inputEl && inputEl.current) {
|
||||
(inputEl.current as any).blur();
|
||||
}
|
||||
const filteredActors = ownerResult
|
||||
?.filter((entity) => entity.urn === selectedValue.value)
|
||||
.map((entity) => entity);
|
||||
if (filteredActors?.length) {
|
||||
const actor = filteredActors[0];
|
||||
const ownerEntityType =
|
||||
actor && actor.type === EntityType.CorpGroup ? OwnerEntityType.CorpGroup : OwnerEntityType.CorpUser;
|
||||
const newValues = [
|
||||
...selectedOwners,
|
||||
{
|
||||
label: selectedValue.value,
|
||||
value: {
|
||||
ownerUrn: selectedValue.value,
|
||||
ownerEntityType,
|
||||
},
|
||||
},
|
||||
];
|
||||
setSelectedOwners(newValues);
|
||||
}
|
||||
};
|
||||
|
||||
// When a owner search result is deselected, remove the Owner
|
||||
const onDeselectOwner = (selectedValue: { key: string; label: React.ReactNode; value: string }) => {
|
||||
setInputValue('');
|
||||
const newValues = selectedOwners.filter((owner) => owner.label !== selectedValue.value);
|
||||
setSelectedOwners(newValues);
|
||||
};
|
||||
|
||||
// When a owner type is selected, set the type as selected type.
|
||||
const onSelectOwnerType = (newType: OwnershipType) => {
|
||||
setSelectedOwnerType(newType);
|
||||
};
|
||||
|
||||
const tagRender = (props) => {
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const { label, closable, onClose } = props;
|
||||
const onPreventMouseDown = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
return (
|
||||
<StyleTag onMouseDown={onPreventMouseDown} closable={closable} onClose={onClose}>
|
||||
{label}
|
||||
</StyleTag>
|
||||
);
|
||||
};
|
||||
|
||||
// Function to handle the modal action's
|
||||
const onOk = async () => {
|
||||
if (selectedOwners.length === 0) {
|
||||
@ -210,35 +207,14 @@ export const AddOwnersModal = ({
|
||||
}
|
||||
};
|
||||
|
||||
const tagRender = (props) => {
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const { label, closable, onClose } = props;
|
||||
const onPreventMouseDown = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
return (
|
||||
<Tag
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{
|
||||
padding: '0px 7px 0px 0px',
|
||||
marginRight: 3,
|
||||
display: 'flex',
|
||||
justifyContent: 'start',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
function handleBlur() {
|
||||
setInputValue('');
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Add Owners"
|
||||
visible={visible}
|
||||
visible
|
||||
onCancel={onModalClose}
|
||||
keyboard
|
||||
footer={
|
||||
@ -252,27 +228,33 @@ export const AddOwnersModal = ({
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Form layout="vertical" form={form} colon={false}>
|
||||
<Form layout="vertical" colon={false}>
|
||||
<Form.Item key="owners" name="owners" label={<Typography.Text strong>Owner</Typography.Text>}>
|
||||
<Typography.Paragraph>Find a user or group</Typography.Paragraph>
|
||||
<Form.Item name="owner">
|
||||
<SelectInput
|
||||
labelInValue
|
||||
value={selectedOwners}
|
||||
autoFocus
|
||||
defaultOpen
|
||||
mode="multiple"
|
||||
filterOption={false}
|
||||
ref={inputEl}
|
||||
placeholder="Search for users or groups..."
|
||||
showSearch
|
||||
filterOption={false}
|
||||
defaultActiveFirstOption={false}
|
||||
onSelect={(asset: any) => onSelectOwner(asset)}
|
||||
onDeselect={(asset: any) => onDeselectOwner(asset)}
|
||||
onSearch={handleActorSearch}
|
||||
onSearch={(value: string) => {
|
||||
// eslint-disable-next-line react/prop-types
|
||||
handleActorSearch(value.trim());
|
||||
// eslint-disable-next-line react/prop-types
|
||||
setInputValue(value.trim());
|
||||
}}
|
||||
tagRender={tagRender}
|
||||
onBlur={handleBlur}
|
||||
value={selectedOwners}
|
||||
>
|
||||
{combinedSearchResults?.map((result) => (
|
||||
<Select.Option key={result?.entity?.urn} value={result.entity.urn}>
|
||||
{renderSearchResult(result)}
|
||||
</Select.Option>
|
||||
))}
|
||||
{ownerSearchOptions}
|
||||
</SelectInput>
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
|
||||
@ -32,17 +32,18 @@ export const SidebarOwnerSection = ({ properties }: { properties?: any }) => {
|
||||
<PlusOutlined /> Add Owners
|
||||
</Button>
|
||||
</div>
|
||||
<AddOwnersModal
|
||||
urn={mutationUrn}
|
||||
defaultOwnerType={properties?.defaultOwnerType}
|
||||
hideOwnerType={properties?.hideOwnerType || false}
|
||||
type={entityType}
|
||||
visible={showAddModal}
|
||||
refetch={refetch}
|
||||
onCloseModal={() => {
|
||||
setShowAddModal(false);
|
||||
}}
|
||||
/>
|
||||
{showAddModal && (
|
||||
<AddOwnersModal
|
||||
urn={mutationUrn}
|
||||
defaultOwnerType={properties?.defaultOwnerType}
|
||||
hideOwnerType={properties?.hideOwnerType || false}
|
||||
type={entityType}
|
||||
refetch={refetch}
|
||||
onCloseModal={() => {
|
||||
setShowAddModal(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
29
datahub-web-react/src/app/shared/DomainLabel.tsx
Normal file
29
datahub-web-react/src/app/shared/DomainLabel.tsx
Normal file
@ -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 (
|
||||
<DomainContainerWrapper>
|
||||
<DomainContentWrapper>
|
||||
<div>{name}</div>
|
||||
</DomainContentWrapper>
|
||||
</DomainContainerWrapper>
|
||||
);
|
||||
};
|
||||
34
datahub-web-react/src/app/shared/OwnerLabel.tsx
Normal file
34
datahub-web-react/src/app/shared/OwnerLabel.tsx
Normal file
@ -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 (
|
||||
<OwnerContainerWrapper>
|
||||
<OwnerContentWrapper>
|
||||
<CustomAvatar size={24} name={name} photoUrl={avatarUrl} isGroup={type === EntityType.CorpGroup} />
|
||||
<div>{name}</div>
|
||||
</OwnerContentWrapper>
|
||||
</OwnerContainerWrapper>
|
||||
);
|
||||
};
|
||||
@ -414,16 +414,17 @@ export default function TagStyleEntity({ urn, useGetSearchResults = useWrappedSe
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<AddOwnersModal
|
||||
hideOwnerType
|
||||
visible={showAddModal}
|
||||
refetch={refetch}
|
||||
onCloseModal={() => {
|
||||
setShowAddModal(false);
|
||||
}}
|
||||
urn={urn}
|
||||
type={EntityType.Tag}
|
||||
/>
|
||||
{showAddModal && (
|
||||
<AddOwnersModal
|
||||
hideOwnerType
|
||||
refetch={refetch}
|
||||
onCloseModal={() => {
|
||||
setShowAddModal(false);
|
||||
}}
|
||||
urn={urn}
|
||||
type={EntityType.Tag}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DetailsLayout>
|
||||
|
||||
18
datahub-web-react/src/app/shared/recommendation.tsx
Normal file
18
datahub-web-react/src/app/shared/recommendation.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { useGetSearchResultsForMultipleQuery } from '../../graphql/search.generated';
|
||||
import { EntityType } from '../../types.generated';
|
||||
|
||||
export const useGetRecommendations = (types: Array<EntityType>) => {
|
||||
const { data } = useGetSearchResultsForMultipleQuery({
|
||||
variables: {
|
||||
input: {
|
||||
types,
|
||||
query: '*',
|
||||
start: 0,
|
||||
count: 5,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const recommendedData = data?.searchAcrossEntities?.searchResults?.map((searchResult) => searchResult.entity) || [];
|
||||
return [recommendedData];
|
||||
};
|
||||
@ -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<string[]>([]);
|
||||
const [selectedTerms, setSelectedTerms] = useState<any[]>([]);
|
||||
const [selectedTags, setSelectedTags] = useState<any[]>([]);
|
||||
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 ? (
|
||||
<TagLabel
|
||||
name={displayName}
|
||||
colorHash={(result.entity as Tag).urn}
|
||||
color={(result.entity as Tag).properties?.colorHex}
|
||||
colorHash={(entity as Tag).urn}
|
||||
color={(entity as Tag).properties?.colorHex}
|
||||
/>
|
||||
) : (
|
||||
<TermLabel name={displayName} />
|
||||
);
|
||||
return (
|
||||
<Select.Option value={result.entity.urn} key={result.entity.urn} name={displayName}>
|
||||
<Select.Option value={entity.urn} key={entity.urn} name={displayName}>
|
||||
{tagOrTermComponent}
|
||||
</Select.Option>
|
||||
);
|
||||
};
|
||||
|
||||
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(
|
||||
<Select.Option value={CREATE_TAG_VALUE} key={CREATE_TAG_VALUE}>
|
||||
<Typography.Link> Create {inputValue}</Typography.Link>
|
||||
</Select.Option>,
|
||||
@ -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 (
|
||||
<CustomTag
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{
|
||||
marginRight: 3,
|
||||
display: 'flex',
|
||||
justifyContent: 'start',
|
||||
alignItems: 'center',
|
||||
whiteSpace: 'nowrap',
|
||||
opacity: 1,
|
||||
color: '#434343',
|
||||
lineHeight: '16px',
|
||||
}}
|
||||
>
|
||||
<StyleTag onMouseDown={onPreventMouseDown} closable={closable} onClose={onClose}>
|
||||
{selectedItem}
|
||||
</CustomTag>
|
||||
</StyleTag>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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: <TermLabel name={selectedSearchOption?.props.name} /> }]);
|
||||
setSelectedTags([
|
||||
...selectedTags,
|
||||
{
|
||||
urn,
|
||||
component: (
|
||||
<TagLabel
|
||||
name={selectedSearchOption?.props.name}
|
||||
colorHash={(selectedTagOption as Tag).urn}
|
||||
color={(selectedTagOption as Tag).properties?.colorHex}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]);
|
||||
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({
|
||||
<ClickOutside onClickOutside={() => setIsFocusedOnInput(false)}>
|
||||
<TagSelect
|
||||
autoFocus
|
||||
defaultOpen
|
||||
mode="multiple"
|
||||
ref={inputEl}
|
||||
filterOption={false}
|
||||
placeholder={`Search for ${entityRegistry.getEntityName(type)?.toLowerCase()}...`}
|
||||
showSearch
|
||||
@ -331,7 +355,7 @@ export default function AddTagsTermsModal({
|
||||
onClear={clearInput}
|
||||
onFocus={() => setIsFocusedOnInput(true)}
|
||||
onBlur={handleBlur}
|
||||
dropdownStyle={isShowingGlossaryBrowser || !inputValue ? { display: 'none' } : {}}
|
||||
dropdownStyle={isShowingGlossaryBrowser ? { display: 'none' } : {}}
|
||||
>
|
||||
{tagSearchOptions}
|
||||
</TagSelect>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user