Skip to content

Commit

Permalink
feat(ui/policies): Add tag filer in policy creation page (#9756)
Browse files Browse the repository at this point in the history
  • Loading branch information
gaurav2733 authored Feb 5, 2024
1 parent fd34e41 commit f4cc60b
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ export const ManagePolicies = () => {
</PaginationContainer>
{showPolicyBuilderModal && (
<PolicyBuilderModal
focusPolicyUrn={focusPolicyUrn}
policy={focusPolicy || EMPTY_POLICY}
setPolicy={setFocusPolicy}
visible={showPolicyBuilderModal}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type Props = {
policy: Omit<Policy, 'urn'>;
setPolicy: (policy: Omit<Policy, 'urn'>) => void;
visible: boolean;
focusPolicyUrn: string | undefined;
onClose: () => void;
onSave: (savePolicy: Omit<Policy, 'urn'>) => void;
};
Expand All @@ -39,9 +40,11 @@ const NextButtonContainer = styled.div`
* Component used for constructing new policies. The purpose of this flow is to populate or edit a Policy
* object through a sequence of steps.
*/
export default function PolicyBuilderModal({ policy, setPolicy, visible, onClose, onSave }: Props) {
export default function PolicyBuilderModal({ policy, setPolicy, visible, onClose, onSave, focusPolicyUrn }: Props) {
// Step control-flow.
const [activeStepIndex, setActiveStepIndex] = useState(0);
const [selectedTags, setSelectedTags] = useState<any[]>([]);
const [isEditState,setEditState] = useState(true)

// Go to next step
const next = () => {
Expand Down Expand Up @@ -90,12 +93,17 @@ export default function PolicyBuilderModal({ policy, setPolicy, visible, onClose
title: 'Configure Privileges',
content: (
<PolicyPrivilegeForm
focusPolicyUrn={focusPolicyUrn}
policyType={policy.type}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
resources={policy.resources!}
setResources={(resources: ResourceFilter) => {
setPolicy({ ...policy, resources });
}}
setSelectedTags={setSelectedTags}
selectedTags={selectedTags}
setEditState={setEditState}
isEditState={isEditState}
privileges={policy.privileges}
setPrivileges={(privileges: string[]) => setPolicy({ ...policy, privileges })}
/>
Expand Down
200 changes: 196 additions & 4 deletions datahub-web-react/src/app/permissions/policy/PolicyPrivilegeForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useMemo, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Link } from 'react-router-dom';
import { Form, Select, Tag, Tooltip, Typography } from 'antd';
import { Form, Select, Tag, Tooltip, Typography, Tag as CustomTag } from 'antd';
import styled from 'styled-components/macro';

import { useEntityRegistry } from '../../useEntityRegistry';
Expand All @@ -9,13 +9,14 @@ import {
useGetSearchResultsForMultipleLazyQuery,
useGetSearchResultsLazyQuery,
} from '../../../graphql/search.generated';
import { ResourceFilter, PolicyType, EntityType, Domain } from '../../../types.generated';
import { ResourceFilter, PolicyType, EntityType, Domain, Entity } from '../../../types.generated';
import {
convertLegacyResourceFilter,
createCriterionValue,
createCriterionValueWithEntity,
EMPTY_POLICY,
getFieldValues,
getFieldValuesOfTags,
mapResourceTypeToDisplayName,
mapResourceTypeToEntityType,
mapResourceTypeToPrivileges,
Expand All @@ -24,20 +25,28 @@ import {
import DomainNavigator from '../../domain/nestedDomains/domainNavigator/DomainNavigator';
import { BrowserWrapper } from '../../shared/tags/AddTagsTermsModal';
import ClickOutside from '../../shared/ClickOutside';
import { TagTermLabel } from '../../shared/tags/TagTermLabel';
import { ENTER_KEY_CODE } from '../../shared/constants';
import { useGetRecommendations } from '../../shared/recommendation';

type Props = {
policyType: PolicyType;
resources?: ResourceFilter;
setResources: (resources: ResourceFilter) => void;
selectedTags?: any[];
setSelectedTags: (data: any) => void;
setEditState: (data: boolean) => void;
isEditState: boolean;
privileges: Array<string>;
setPrivileges: (newPrivs: Array<string>) => void;
focusPolicyUrn: string | undefined;
};

const SearchResultContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
padding: 4px;
`;

const PrivilegesForm = styled(Form)`
Expand All @@ -46,6 +55,21 @@ const PrivilegesForm = styled(Form)`
margin-bottom: 40px;
`;

const TagSelect = styled(Select)`
width: 480px;
`;

const StyleTag = styled(CustomTag)`
margin: 2px;
display: flex;
justify-content: start;
align-items: center;
white-space: nowrap;
opacity: 1;
color: #434343;
line-height: 16px;
`;

/**
* Component used to construct the "privileges" and "resources" portion of a DataHub
* access Policy.
Expand All @@ -56,10 +80,21 @@ export default function PolicyPrivilegeForm({
setResources,
privileges,
setPrivileges,
setSelectedTags,
selectedTags,
setEditState,
isEditState,
focusPolicyUrn,
}: Props) {
const entityRegistry = useEntityRegistry();
const [domainInputValue, setDomainInputValue] = useState('');
const [isFocusedOnInput, setIsFocusedOnInput] = useState(false);
const [inputValue, setInputValue] = useState('');
const [tagTermSearch, { data: tagTermSearchData }] = useGetSearchResultsLazyQuery();
const [recommendedData] = useGetRecommendations([EntityType.Tag]);
const tagSearchResults = tagTermSearchData?.search?.searchResults?.map((searchResult) => searchResult.entity) || [];

const inputEl = useRef(null);

// Configuration used for displaying options
const {
Expand Down Expand Up @@ -295,6 +330,131 @@ export default function PolicyPrivilegeForm({
setDomainInputValue('');
}

function handleBlurTag() {
setInputValue('');
}

const renderSearchResultTags = (entity: Entity) => {
const displayName =
entity.type === EntityType.Tag ? (entity as any).name : entityRegistry.getDisplayName(entity.type, entity);
const tagOrTermComponent = <TagTermLabel entity={entity} />;
return (
<Select.Option data-testid="tag-term-option" value={entity.urn} key={entity.urn} name={displayName}>
<SearchResultContainer>{tagOrTermComponent}</SearchResultContainer>
</Select.Option>
);
};
const tags = getFieldValues(resources.filter, 'TAG') || [];
const newTag = getFieldValues(resources.filter, 'TAG').map((criterionValue) => {
if (criterionValue?.value) {
return criterionValue?.value;
}
return criterionValue;
});

const editTags = getFieldValuesOfTags(resources.filter, 'TAG').map((criterionValue) => {
if (criterionValue?.value) {
return criterionValue?.entity;
}
return criterionValue;
});
const tagResult = !inputValue || inputValue.length === 0 ? recommendedData : tagSearchResults;
useEffect(() => {
if (focusPolicyUrn && isEditState && setEditState && editTags && newTag) {
setEditState(false);
const filter = resources.filter || {
criteria: [],
};
setSelectedTags(editTags);
setResources({
...resources,
filter: setFieldValues(filter, 'TAG', [...(newTag as any)]),
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [focusPolicyUrn, isEditState, setEditState, editTags, newTag]);

const onSelectValue = (urn: string) => {
const filter = resources.filter || {
criteria: [],
};
const selectedTagOption = tagResult?.find((tag) => tag.urn === urn);

setResources({
...resources,
filter: setFieldValues(filter, 'TAG', [...tags, urn as any]),
});
setSelectedTags([...(selectedTags as any), selectedTagOption]);
if (inputEl && inputEl.current) {
(inputEl.current as any).blur();
}
};

// When a Tag search result is deselected, remove the Tags
const onDeselectValue = (urn: string) => {
const filter = resources.filter || {
criteria: [],
};
setInputValue('');
setSelectedTags(selectedTags?.filter((term) => term.urn !== urn));

setResources({
...resources,
filter: setFieldValues(
filter,
'TAG',
tags?.filter((criterionValue) => (criterionValue as any) !== urn),
),
});
};

const type = EntityType.Tag;
const handleSearch = (text: string) => {
if (text.length > 0) {
tagTermSearch({
variables: {
input: {
type,
query: text,
start: 0,
count: 10,
},
},
});
}
};

const tagSearchOptions = tagResult?.map((result) => {
return renderSearchResultTags(result);
});

function clearInput() {
setInputValue('');
setTimeout(() => setIsFocusedOnInput(true), 0); // call after click outside
}

const tagRender = (props) => {
// eslint-disable-next-line react/prop-types
const { closable, onClose, value } = props;
const onPreventMouseDown = (event) => {
event.preventDefault();
event.stopPropagation();
};

const selectedItem = selectedTags?.find((term) => term?.urn === value);
return (
<StyleTag onMouseDown={onPreventMouseDown} closable={closable} onClose={onClose}>
{selectedItem?.name}
</StyleTag>
);
};

function handleKeyDown(event) {
if (event.keyCode === ENTER_KEY_CODE) {
(inputEl.current as any).blur();
}
}

return (
<PrivilegesForm layout="vertical">
{showResourceFilterInput && (
Expand Down Expand Up @@ -362,6 +522,38 @@ export default function PolicyPrivilegeForm({
</Select>
</Form.Item>
)}
{showResourceFilterInput && (
<Form.Item label={<Typography.Text strong>Select Tags</Typography.Text>}>
<Typography.Paragraph>
The policy will apply to all entities containing all of the chosen tags. If no tags are
selected, the policy will not account for tags.
</Typography.Paragraph>
<TagSelect
data-testid="tag-term-modal-input"
mode="multiple"
ref={inputEl}
filterOption={false}
placeholder={`Search for ${entityRegistry.getEntityName(type)?.toLowerCase()}...`}
showSearch
defaultActiveFirstOption={false}
onSelect={(asset: any) => onSelectValue(asset)}
onDeselect={(asset: any) => onDeselectValue(asset)}
onSearch={(value: string) => {
// eslint-disable-next-line react/prop-types
handleSearch(value.trim());
// eslint-disable-next-line react/prop-types
setInputValue(value.trim());
}}
tagRender={tagRender}
value={tags}
onClear={clearInput}
onBlur={handleBlurTag}
onInputKeyDown={handleKeyDown}
>
{tagSearchOptions}
</TagSelect>
</Form.Item>
)}
{showResourceFilterInput && (
<Form.Item label={<Typography.Text strong>Select Domains</Typography.Text>}>
<Typography.Paragraph>
Expand Down
4 changes: 4 additions & 0 deletions datahub-web-react/src/app/permissions/policy/policyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ export const getFieldValues = (filter: Maybe<PolicyMatchFilter> | undefined, res
return filter?.criteria?.find((criterion) => criterion.field === resourceFieldType)?.values || [];
};

export const getFieldValuesOfTags = (filter: Maybe<PolicyMatchFilter> | undefined, resourceFieldType: string) => {
return filter?.criteria?.find((criterion) => criterion.field === resourceFieldType)?.values || [];
};

export const setFieldValues = (
filter: PolicyMatchFilter,
resourceFieldType: string,
Expand Down
14 changes: 9 additions & 5 deletions datahub-web-react/src/app/permissions/policy/usePolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,22 @@ export function usePolicy(

const [deletePolicy, { error: deletePolicyError }] = useDeletePolicyMutation();

const toFilterInput = (filter: PolicyMatchFilter): PolicyMatchFilterInput => {
const toFilterInput = (filter: PolicyMatchFilter,state?:string | undefined): PolicyMatchFilterInput => {
console.log({state})
return {
criteria: filter.criteria?.map((criterion): PolicyMatchCriterionInput => {
return {
field: criterion.field,
values: criterion.values.map((criterionValue) => criterionValue.value),
values: criterion.values.map((criterionValue) =>
criterion.field === 'TAG' && state !=='TOGGLE' ? (criterionValue as any) : criterionValue.value,
),
condition: criterion.condition,
};
}),
};
};

const toPolicyInput = (policy: Omit<Policy, 'urn'>): PolicyUpdateInput => {
const toPolicyInput = (policy: Omit<Policy, 'urn'>,state?:string | undefined): PolicyUpdateInput => {
let policyInput: PolicyUpdateInput = {
type: policy.type,
name: policy.name,
Expand All @@ -79,7 +82,7 @@ export function usePolicy(
allResources: policy.resources.allResources,
};
if (policy.resources.filter) {
resourceFilter = { ...resourceFilter, filter: toFilterInput(policy.resources.filter) };
resourceFilter = { ...resourceFilter, filter: toFilterInput(policy.resources.filter,state) };
}
// Add the resource filters.
policyInput = {
Expand Down Expand Up @@ -151,7 +154,7 @@ export function usePolicy(
updatePolicy({
variables: {
urn: policy?.urn as string, // There must be a focus policy urn.
input: toPolicyInput(newPolicy),
input: toPolicyInput(newPolicy,'TOGGLE'),
},
}).then(()=>{
const updatePolicies= {
Expand All @@ -178,6 +181,7 @@ export function usePolicy(
__typename: 'ListPoliciesResult',
urn: focusPolicyUrn,
...savePolicy,
resources: null,
};
analytics.event({
type: EventType.UpdatePolicyEvent,
Expand Down

0 comments on commit f4cc60b

Please sign in to comment.