feat(previews): add previews for glossary terms, tags, and domains (#5784)

* add graphql for chart glossary terms

* add placeholder tab component

* continued progress

* finished rendering of other tooltips

* fix tests

* add null check

* force more clicks

* fix analytics test
This commit is contained in:
Gabe Lyons 2022-08-31 20:48:32 -07:00 committed by GitHub
parent ca29f8b679
commit 5bf5fc2d66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 586 additions and 216 deletions

View File

@ -71,7 +71,8 @@ public class ChartType implements SearchableEntityType<Chart, String>, Browsable
CONTAINER_ASPECT_NAME,
DOMAINS_ASPECT_NAME,
DEPRECATION_ASPECT_NAME,
DATA_PLATFORM_INSTANCE_ASPECT_NAME
DATA_PLATFORM_INSTANCE_ASPECT_NAME,
INPUT_FIELDS_ASPECT_NAME
);
private static final Set<String> FACET_FIELDS = ImmutableSet.of("access", "queryType", "tool", "type");

View File

@ -5,6 +5,7 @@ import com.linkedin.common.DataPlatformInstance;
import com.linkedin.common.Deprecation;
import com.linkedin.common.GlobalTags;
import com.linkedin.common.GlossaryTerms;
import com.linkedin.common.InputFields;
import com.linkedin.common.InstitutionalMemory;
import com.linkedin.common.Ownership;
import com.linkedin.common.Status;
@ -86,6 +87,8 @@ public class ChartMapper implements ModelMapper<EntityResponse, Chart> {
chart.setDeprecation(DeprecationMapper.map(new Deprecation(dataMap))));
mappingHelper.mapToResult(DATA_PLATFORM_INSTANCE_ASPECT_NAME, (dataset, dataMap) ->
dataset.setDataPlatformInstance(DataPlatformInstanceAspectMapper.map(new DataPlatformInstance(dataMap))));
mappingHelper.mapToResult(INPUT_FIELDS_ASPECT_NAME, (chart, dataMap) ->
chart.setInputFields(InputFieldsMapper.map(new InputFields(dataMap), entityUrn)));
return mappingHelper.getResult();
}

View File

@ -0,0 +1,32 @@
package com.linkedin.datahub.graphql.types.chart.mappers;
import com.linkedin.common.InputFields;
import com.linkedin.common.urn.Urn;
import com.linkedin.datahub.graphql.generated.InputField;
import com.linkedin.datahub.graphql.types.dataset.mappers.SchemaFieldMapper;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
public class InputFieldsMapper {
public static final InputFieldsMapper INSTANCE = new InputFieldsMapper();
public static com.linkedin.datahub.graphql.generated.InputFields map(@Nonnull final InputFields metadata, @Nonnull final Urn entityUrn) {
return INSTANCE.apply(metadata, entityUrn);
}
public com.linkedin.datahub.graphql.generated.InputFields apply(@Nonnull final InputFields input, @Nonnull final Urn entityUrn) {
final com.linkedin.datahub.graphql.generated.InputFields result = new com.linkedin.datahub.graphql.generated.InputFields();
result.setFields(input.getFields().stream().map(field -> {
InputField fieldResult = new InputField();
if (field.hasSchemaField()) {
fieldResult.setSchemaField(SchemaFieldMapper.map(field.getSchemaField(), entityUrn));
}
return fieldResult;
}).collect(Collectors.toList()));
return result;
}
}

View File

@ -72,7 +72,8 @@ public class DashboardType implements SearchableEntityType<Dashboard, String>, B
CONTAINER_ASPECT_NAME,
DOMAINS_ASPECT_NAME,
DEPRECATION_ASPECT_NAME,
DATA_PLATFORM_INSTANCE_ASPECT_NAME
DATA_PLATFORM_INSTANCE_ASPECT_NAME,
INPUT_FIELDS_ASPECT_NAME
);
private static final Set<String> FACET_FIELDS = ImmutableSet.of("access", "tool");

View File

@ -4,6 +4,7 @@ import com.linkedin.common.DataPlatformInstance;
import com.linkedin.common.Deprecation;
import com.linkedin.common.GlobalTags;
import com.linkedin.common.GlossaryTerms;
import com.linkedin.common.InputFields;
import com.linkedin.common.InstitutionalMemory;
import com.linkedin.common.Ownership;
import com.linkedin.common.Status;
@ -19,6 +20,7 @@ import com.linkedin.datahub.graphql.generated.DashboardInfo;
import com.linkedin.datahub.graphql.generated.DashboardProperties;
import com.linkedin.datahub.graphql.generated.DataPlatform;
import com.linkedin.datahub.graphql.generated.EntityType;
import com.linkedin.datahub.graphql.types.chart.mappers.InputFieldsMapper;
import com.linkedin.datahub.graphql.types.common.mappers.AuditStampMapper;
import com.linkedin.datahub.graphql.types.common.mappers.DataPlatformInstanceAspectMapper;
import com.linkedin.datahub.graphql.types.common.mappers.DeprecationMapper;
@ -82,6 +84,8 @@ public class DashboardMapper implements ModelMapper<EntityResponse, Dashboard> {
mappingHelper.mapToResult(GLOBAL_TAGS_ASPECT_NAME, (dataset, dataMap) -> this.mapGlobalTags(dataset, dataMap, entityUrn));
mappingHelper.mapToResult(DATA_PLATFORM_INSTANCE_ASPECT_NAME, (dataset, dataMap) ->
dataset.setDataPlatformInstance(DataPlatformInstanceAspectMapper.map(new DataPlatformInstance(dataMap))));
mappingHelper.mapToResult(INPUT_FIELDS_ASPECT_NAME, (dashboard, dataMap) ->
dashboard.setInputFields(InputFieldsMapper.map(new InputFields(dataMap), entityUrn)));
return mappingHelper.getResult();
}

View File

@ -25,6 +25,7 @@ public class SchemaFieldMapper {
result.setNullable(input.isNullable());
result.setNativeDataType(input.getNativeDataType());
result.setType(mapSchemaFieldDataType(input.getType()));
result.setLabel(input.getLabel());
if (input.hasGlobalTags()) {
result.setGlobalTags(GlobalTagsMapper.map(input.getGlobalTags(), entityUrn));
result.setTags(GlobalTagsMapper.map(input.getGlobalTags(), entityUrn));

View File

@ -2319,6 +2319,11 @@ type SchemaField {
"""
jsonPath: String
"""
Human readable label for the field. Not supplied by all data sources
"""
label: String
"""
Indicates if this field is optional or nullable
"""
@ -4332,6 +4337,11 @@ type Dashboard implements EntityWithRelationships & Entity & BrowsableEntity {
Standardized platform urn where the dashboard is defined
"""
platform: DataPlatform!
"""
Input fields that power all the charts in the dashboard
"""
inputFields: InputFields
}
"""
@ -4612,6 +4622,11 @@ type Chart implements EntityWithRelationships & Entity & BrowsableEntity {
Standardized platform urn where the chart is defined
"""
platform: DataPlatform!
"""
Input fields to power the chart
"""
inputFields: InputFields
}
"""
@ -9188,6 +9203,21 @@ input BatchUpdateSoftDeletedInput {
deleted: Boolean!
}
"""
Input fields of the chart
"""
type InputFields {
fields: [InputField]
}
"""
Input field of the chart
"""
type InputField {
schemaFieldUrn: String
schemaField: SchemaField
}
enum UserSetting {
"""
Show simplified homepage

View File

@ -409,7 +409,10 @@ export const dataset3 = {
description: 'sample definition',
definition: 'sample definition',
termSource: 'sample term source',
customProperties: null,
},
ownership: null,
parentNodes: null,
},
associatedUrn: 'urn:li:dataset:3',
},
@ -439,6 +442,7 @@ export const dataset3 = {
createdAt: 0,
fields: [
{
__typename: 'SchemaField',
nullable: false,
recursive: false,
fieldPath: 'user_id',
@ -449,8 +453,10 @@ export const dataset3 = {
jsonPath: null,
globalTags: null,
glossaryTerms: null,
label: 'hi',
},
{
__typename: 'SchemaField',
nullable: false,
recursive: false,
fieldPath: 'user_name',
@ -461,6 +467,7 @@ export const dataset3 = {
jsonPath: null,
globalTags: null,
glossaryTerms: null,
label: 'hi',
},
],
hash: '',
@ -862,6 +869,7 @@ const glossaryTerm1 = {
sourceRef: 'sourceRef',
sourceURI: 'sourceURI',
},
parentNodes: null,
deprecation: null,
} as GlossaryTerm;
@ -934,6 +942,7 @@ const glossaryTerm2 = {
],
__typename: 'EntityRelationshipsResult',
},
parentNodes: null,
__typename: 'GlossaryTerm',
};

View File

@ -19,6 +19,10 @@ export enum PreviewType {
* A tiny search preview for text-box search.
*/
MINI_SEARCH,
/**
* Previews rendered when hovering over the entity in a compact list
*/
HOVER_CARD,
}
export enum IconStyleType {

View File

@ -1,6 +1,5 @@
import { LineChartOutlined } from '@ant-design/icons';
import * as React from 'react';
import { Typography } from 'antd';
import { Chart, EntityType, SearchResult } from '../../../types.generated';
import { Entity, EntityCapabilityType, IconStyleType, PreviewType } from '../Entity';
@ -20,8 +19,8 @@ import { SidebarDomainSection } from '../shared/containers/profile/sidebar/Domai
import { EntityMenuItems } from '../shared/EntityDropdown/EntityDropdown';
import { LineageTab } from '../shared/tabs/Lineage/LineageTab';
import { ChartStatsSummarySubHeader } from './profile/stats/ChartStatsSummarySubHeader';
import { getMatchPrioritizingPrimary } from '../shared/utils';
import { FIELDS_TO_HIGHLIGHT } from '../dataset/search/highlights';
import { InputFieldsTab } from '../shared/tabs/Entity/InputFieldsTab';
import { ChartSnippet } from './ChartSnippet';
/**
* Definition of the DataHub Chart entity.
@ -84,6 +83,14 @@ export class ChartEntity implements Entity<Chart> {
name: 'Documentation',
component: DocumentationTab,
},
{
name: 'Fields',
component: InputFieldsTab,
display: {
visible: (_, chart: GetChartQuery) => (chart?.chart?.inputFields?.fields?.length || 0) > 0,
enabled: (_, chart: GetChartQuery) => (chart?.chart?.inputFields?.fields?.length || 0) > 0,
},
},
{
name: 'Properties',
component: PropertiesTab,
@ -169,8 +176,6 @@ export class ChartEntity implements Entity<Chart> {
renderSearch = (result: SearchResult) => {
const data = result.entity as Chart;
const matchedField = getMatchPrioritizingPrimary(result.matchedFields, 'fieldLabels');
return (
<ChartPreview
urn={data.urn}
@ -190,13 +195,7 @@ export class ChartEntity implements Entity<Chart> {
lastUpdatedMs={data.properties?.lastModified?.time}
createdMs={data.properties?.created?.time}
externalUrl={data.properties?.externalUrl}
snippet={
matchedField && (
<Typography.Text>
Matches {FIELDS_TO_HIGHLIGHT.get(matchedField.name)} <b>{matchedField.value}</b>
</Typography.Text>
)
}
snippet={<ChartSnippet matchedFields={result.matchedFields} inputFields={data.inputFields} />}
/>
);
};

View File

@ -0,0 +1,50 @@
import React from 'react';
import { Typography } from 'antd';
import { InputFields, MatchedField, Maybe } from '../../../types.generated';
import TagTermGroup from '../../shared/tags/TagTermGroup';
import { FIELDS_TO_HIGHLIGHT } from '../dataset/search/highlights';
import { getMatchPrioritizingPrimary } from '../shared/utils';
type Props = {
matchedFields: MatchedField[];
inputFields: Maybe<InputFields> | undefined;
};
const LABEL_INDEX_NAME = 'fieldLabels';
const TYPE_PROPERTY_KEY_NAME = 'type';
export const ChartSnippet = ({ matchedFields, inputFields }: Props) => {
const matchedField = getMatchPrioritizingPrimary(matchedFields, 'fieldLabels');
if (matchedField?.name === LABEL_INDEX_NAME) {
const matchedSchemaField = inputFields?.fields?.find(
(field) => field?.schemaField?.label === matchedField.value,
);
const matchedGlossaryTerm = matchedSchemaField?.schemaField?.glossaryTerms?.terms?.find(
(term) => term?.term?.name === matchedField.value,
);
if (matchedGlossaryTerm) {
let termType = 'term';
const typeProperty = matchedGlossaryTerm.term.properties?.customProperties?.find(
(property) => property.key === TYPE_PROPERTY_KEY_NAME,
);
if (typeProperty) {
termType = typeProperty.value || termType;
}
return (
<Typography.Text>
Matches {termType} <TagTermGroup uneditableGlossaryTerms={{ terms: [matchedGlossaryTerm] }} />
</Typography.Text>
);
}
}
return matchedField ? (
<Typography.Text>
Matches {FIELDS_TO_HIGHLIGHT.get(matchedField.name)} <b>{matchedField.value}</b>
</Typography.Text>
) : null;
};

View File

@ -1,6 +1,5 @@
import { DashboardFilled, DashboardOutlined } from '@ant-design/icons';
import * as React from 'react';
import { Typography } from 'antd';
import {
GetDashboardQuery,
@ -24,8 +23,8 @@ import { SidebarDomainSection } from '../shared/containers/profile/sidebar/Domai
import { EntityMenuItems } from '../shared/EntityDropdown/EntityDropdown';
import { LineageTab } from '../shared/tabs/Lineage/LineageTab';
import { DashboardStatsSummarySubHeader } from './profile/DashboardStatsSummarySubHeader';
import { FIELDS_TO_HIGHLIGHT } from '../dataset/search/highlights';
import { getMatchPrioritizingPrimary } from '../shared/utils';
import { InputFieldsTab } from '../shared/tabs/Entity/InputFieldsTab';
import { ChartSnippet } from '../chart/ChartSnippet';
/**
* Definition of the DataHub Dashboard entity.
@ -88,6 +87,16 @@ export class DashboardEntity implements Entity<Dashboard> {
name: 'Documentation',
component: DocumentationTab,
},
{
name: 'Fields',
component: InputFieldsTab,
display: {
visible: (_, dashboard: GetDashboardQuery) =>
(dashboard?.dashboard?.inputFields?.fields?.length || 0) > 0,
enabled: (_, dashboard: GetDashboardQuery) =>
(dashboard?.dashboard?.inputFields?.fields?.length || 0) > 0,
},
},
{
name: 'Properties',
component: PropertiesTab,
@ -184,7 +193,6 @@ export class DashboardEntity implements Entity<Dashboard> {
renderSearch = (result: SearchResult) => {
const data = result.entity as Dashboard;
const matchedField = getMatchPrioritizingPrimary(result.matchedFields, 'fieldLabels');
return (
<DashboardPreview
@ -207,13 +215,7 @@ export class DashboardEntity implements Entity<Dashboard> {
statsSummary={data.statsSummary}
lastUpdatedMs={data.properties?.lastModified?.time}
createdMs={data.properties?.created?.time}
snippet={
matchedField && (
<Typography.Text>
Matches {FIELDS_TO_HIGHLIGHT.get(matchedField.name)} <b>{matchedField.value}</b>
</Typography.Text>
)
}
snippet={<ChartSnippet matchedFields={result.matchedFields} inputFields={data.inputFields} />}
/>
);
};

View File

@ -114,9 +114,11 @@ class GlossaryNodeEntity implements Entity<GlossaryNode> {
};
renderPreview = (_: PreviewType, data: GlossaryNode) => {
console.log(data);
return (
<Preview
urn={data?.urn}
parentNodes={data.parentNodes}
name={this.displayName(data)}
description={data?.properties?.description || ''}
owners={data?.ownership?.owners}

View File

@ -1,6 +1,6 @@
import React from 'react';
import { FolderOutlined } from '@ant-design/icons';
import { EntityType, Owner } from '../../../../types.generated';
import { EntityType, Owner, ParentNodesResult } from '../../../../types.generated';
import DefaultPreviewCard from '../../../preview/DefaultPreviewCard';
import { useEntityRegistry } from '../../../useEntityRegistry';
@ -9,11 +9,13 @@ export const Preview = ({
name,
description,
owners,
parentNodes,
}: {
urn: string;
name: string;
description?: string | null;
owners?: Array<Owner> | null;
parentNodes?: ParentNodesResult | null;
}): JSX.Element => {
const entityRegistry = useEntityRegistry();
return (
@ -24,6 +26,7 @@ export const Preview = ({
owners={owners}
logoComponent={<FolderOutlined style={{ fontSize: '20px' }} />}
type={entityRegistry.getEntityName(EntityType.GlossaryNode)}
parentNodes={parentNodes}
/>
);
};

View File

@ -130,10 +130,12 @@ export class GlossaryTermEntity implements Entity<GlossaryTerm> {
return this.renderPreview(PreviewType.SEARCH, result.entity as GlossaryTerm);
};
renderPreview = (_: PreviewType, data: GlossaryTerm) => {
renderPreview = (previewType: PreviewType, data: GlossaryTerm) => {
return (
<Preview
previewType={previewType}
urn={data?.urn}
parentNodes={data.parentNodes}
name={this.displayName(data)}
description={data?.properties?.description || ''}
owners={data?.ownership?.owners}

View File

@ -1,9 +1,9 @@
import React from 'react';
import { BookOutlined } from '@ant-design/icons';
import { Deprecation, EntityType, Owner } from '../../../../types.generated';
import { Deprecation, EntityType, Owner, ParentNodesResult } from '../../../../types.generated';
import DefaultPreviewCard from '../../../preview/DefaultPreviewCard';
import { useEntityRegistry } from '../../../useEntityRegistry';
import { IconStyleType } from '../../Entity';
import { IconStyleType, PreviewType } from '../../Entity';
export const Preview = ({
urn,
@ -11,16 +11,21 @@ export const Preview = ({
description,
owners,
deprecation,
parentNodes,
previewType,
}: {
urn: string;
name: string;
description?: string | null;
owners?: Array<Owner> | null;
deprecation?: Deprecation | null;
parentNodes?: ParentNodesResult | null;
previewType: PreviewType;
}): JSX.Element => {
const entityRegistry = useEntityRegistry();
return (
<DefaultPreviewCard
previewType={previewType}
url={entityRegistry.getEntityUrl(EntityType.GlossaryTerm, urn)}
name={name || ''}
description={description || ''}
@ -29,6 +34,7 @@ export const Preview = ({
type="Glossary Term"
typeIcon={entityRegistry.getIcon(EntityType.GlossaryTerm, 14, IconStyleType.ACCENT)}
deprecation={deprecation}
parentNodes={parentNodes}
/>
);
};

View File

@ -2,10 +2,10 @@ import React from 'react';
import removeMd from '@tommoor/remove-markdown';
import styled from 'styled-components';
const RemoveMarkdownContainer = styled.div`
const RemoveMarkdownContainer = styled.div<{ shouldWrap: boolean }>`
display: block;
overflow-wrap: break-word;
white-space: nowrap;
white-space: ${(props) => (props.shouldWrap ? 'normal' : 'nowrap')};
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
@ -16,6 +16,7 @@ export type Props = {
readMore?: JSX.Element;
suffix?: JSX.Element;
limit?: number;
shouldWrap?: boolean;
};
export const removeMarkdown = (text: string) => {
@ -28,7 +29,7 @@ export const removeMarkdown = (text: string) => {
.replace(/^•/, ''); // remove first •
};
export default function NoMarkdownViewer({ children, readMore, suffix, limit }: Props) {
export default function NoMarkdownViewer({ children, readMore, suffix, limit, shouldWrap }: Props) {
let plainText = removeMarkdown(children || '');
if (limit) {
@ -42,7 +43,7 @@ export default function NoMarkdownViewer({ children, readMore, suffix, limit }:
const showReadMore = plainText.length >= (limit || 0);
return (
<RemoveMarkdownContainer>
<RemoveMarkdownContainer shouldWrap={!!shouldWrap}>
{plainText} {showReadMore && <>{readMore}</>} {suffix}
</RemoveMarkdownContainer>
);

View File

@ -1,9 +1,9 @@
import React from 'react';
import styled from 'styled-components';
import { Typography, Image, Tooltip } from 'antd';
import { RightOutlined } from '@ant-design/icons';
import { FolderOutlined, RightOutlined } from '@ant-design/icons';
import { Maybe } from 'graphql/jsutils/Maybe';
import { Container } from '../../../../../../../types.generated';
import { Container, GlossaryNode } from '../../../../../../../types.generated';
import { ANTD_GRAY } from '../../../../constants';
import ContainerLink from './ContainerLink';
import { capitalizeFirstLetterOnly } from '../../../../../../shared/textUtil';
@ -70,6 +70,21 @@ const StyledTooltip = styled(Tooltip)`
overflow: hidden;
`;
const GlossaryNodeText = styled(Typography.Text)`
font-size: 12px;
line-height: 20px;
color: ${ANTD_GRAY[7]};
`;
const GlossaryNodeIcon = styled(FolderOutlined)`
color: ${ANTD_GRAY[7]};
&&& {
font-size: 12px;
margin-right: 4px;
}
`;
export function getParentContainerNames(containers?: Maybe<Container>[] | null) {
let parentNames = '';
if (containers) {
@ -95,12 +110,14 @@ interface Props {
typeIcon?: JSX.Element;
entityType?: string;
parentContainers?: Maybe<Container>[] | null;
parentNodes?: GlossaryNode[] | null;
parentContainersRef: React.RefObject<HTMLDivElement>;
areContainersTruncated: boolean;
}
function PlatformContentView(props: Props) {
const {
parentNodes,
platformName,
platformLogoUrl,
platformNames,
@ -121,7 +138,9 @@ function PlatformContentView(props: Props) {
<PlatformContentWrapper>
{typeIcon && <LogoIcon>{typeIcon}</LogoIcon>}
<PlatformText>{capitalizeFirstLetterOnly(entityType)}</PlatformText>
{(!!platformName || !!instanceId || !!parentContainers?.length) && <PlatformDivider />}
{(!!platformName || !!instanceId || !!parentContainers?.length || !!parentNodes?.length) && (
<PlatformDivider />
)}
{platformName && (
<LogoIcon>
{!platformLogoUrl && !platformLogoUrls && entityLogoComponent}
@ -162,6 +181,13 @@ function PlatformContentView(props: Props) {
</ParentContainersWrapper>
{directParentContainer && <ContainerLink container={directParentContainer} />}
</StyledTooltip>
{[...(parentNodes || [])]?.reverse()?.map((parentNode, idx) => (
<>
<GlossaryNodeIcon />
<GlossaryNodeText>{parentNode?.properties?.name}</GlossaryNodeText>
{idx + 1 !== parentNodes?.length && <StyledRightOutlined data-testid="right-arrow" />}
</>
))}
</PlatformContentWrapper>
);
}

View File

@ -5,14 +5,11 @@ import { EMPTY_MESSAGES } from '../../../../constants';
import { useEntityData, useMutationUrn, useRefetch } from '../../../../EntityContext';
import { SidebarHeader } from '../SidebarHeader';
import { SetDomainModal } from './SetDomainModal';
import { useEntityRegistry } from '../../../../../../useEntityRegistry';
import { EntityType } from '../../../../../../../types.generated';
import { useUnsetDomainMutation } from '../../../../../../../graphql/mutations.generated';
import { DomainLink } from '../../../../../../shared/tags/DomainLink';
export const SidebarDomainSection = () => {
const { entityData } = useEntityData();
const entityRegistry = useEntityRegistry();
const refetch = useRefetch();
const urn = useMutationUrn();
const [unsetDomainMutation] = useUnsetDomainMutation();
@ -53,8 +50,7 @@ export const SidebarDomainSection = () => {
<div>
{domain && (
<DomainLink
urn={domain?.urn}
name={entityRegistry.getDisplayName(EntityType.Domain, domain)}
domain={domain}
closable
onClose={(e) => {
e.preventDefault();

View File

@ -0,0 +1,52 @@
import { Empty } from 'antd';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { SchemaField } from '../../../../../types.generated';
import SchemaEditableContext from '../../../../shared/SchemaEditableContext';
import { groupByFieldPath } from '../../../dataset/profile/schema/utils/utils';
import { ANTD_GRAY } from '../../constants';
import { useEntityData } from '../../EntityContext';
import SchemaTable from '../Dataset/Schema/SchemaTable';
const NoSchema = styled(Empty)`
color: ${ANTD_GRAY[6]};
padding-top: 60px;
`;
const SchemaTableContainer = styled.div`
overflow: auto;
height: 100%;
`;
export const InputFieldsTab = () => {
const { entityData } = useEntityData();
const inputFields = entityData?.inputFields || undefined;
const ungroupedRows = inputFields?.fields?.map((field) => field?.schemaField) as SchemaField[];
const rows = useMemo(() => {
return groupByFieldPath(ungroupedRows, { showKeySchema: false });
}, [ungroupedRows]);
return (
<>
<SchemaTableContainer>
{rows && rows.length > 0 ? (
<>
<SchemaEditableContext.Provider value={false}>
<SchemaTable
schemaMetadata={null}
rows={rows}
editMode={false}
editableSchemaMetadata={null}
usageStats={null}
schemaFieldBlameList={null}
showSchemaAuditView={false}
/>
</SchemaEditableContext.Provider>
</>
) : (
<NoSchema />
)}
</SchemaTableContainer>
</>
);
};

View File

@ -30,6 +30,7 @@ import {
SiblingProperties,
CustomPropertiesEntry,
DomainAssociation,
InputFields,
} from '../../../types.generated';
import { FetchedEntity } from '../../lineage/types';
@ -95,6 +96,7 @@ export type GenericEntityProperties = {
siblings?: Maybe<SiblingProperties>;
siblingPlatforms?: Maybe<DataPlatform[]>;
lastIngested?: Maybe<number>;
inputFields?: Maybe<InputFields>;
};
export type GenericEntityUpdate = {

View File

@ -1,4 +1,4 @@
import React, { ReactNode } from 'react';
import React, { ReactNode, useState } from 'react';
import { Button, Divider, Tooltip, Typography } from 'antd';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
@ -15,6 +15,7 @@ import {
CorpUser,
Deprecation,
Domain,
ParentNodesResult,
} from '../../types.generated';
import TagTermGroup from '../shared/tags/TagTermGroup';
import { ANTD_GRAY } from '../entity/shared/constants';
@ -26,6 +27,7 @@ import { useParentContainersTruncation } from '../entity/shared/containers/profi
import EntityCount from '../entity/shared/containers/profile/header/EntityCount';
import { ExpandedActorGroup } from '../entity/shared/components/styled/ExpandedActorGroup';
import { DeprecationPill } from '../entity/shared/components/styled/DeprecationPill';
import { PreviewType } from '../entity/Entity';
const PreviewContainer = styled.div`
display: flex;
@ -185,6 +187,8 @@ interface Props {
// how the listed node is connected to the source node
degree?: number;
parentContainers?: ParentContainersResult | null;
parentNodes?: ParentNodesResult | null;
previewType?: Maybe<PreviewType>;
}
export default function DefaultPreviewCard({
@ -217,8 +221,10 @@ export default function DefaultPreviewCard({
onClick,
degree,
parentContainers,
parentNodes,
platforms,
logoUrls,
previewType,
}: Props) {
// sometimes these lists will be rendered inside an entity container (for example, in the case of impact analysis)
// in those cases, we may want to enrich the preview w/ context about the container entity
@ -236,6 +242,7 @@ export default function DefaultPreviewCard({
if (snippet) {
insightViews.push(snippet);
}
const [descriptionExpanded, setDescriptionExpanded] = useState(false);
const { parentContainersRef, areContainersTruncated } = useParentContainersTruncation(container);
@ -258,6 +265,7 @@ export default function DefaultPreviewCard({
typeIcon={typeIcon}
entityType={type}
parentContainers={parentContainers?.containers}
parentNodes={parentNodes?.nodes}
parentContainersRef={parentContainersRef}
areContainersTruncated={areContainersTruncated}
/>
@ -291,7 +299,24 @@ export default function DefaultPreviewCard({
</TitleContainer>
{description && description.length > 0 && (
<DescriptionContainer>
<NoMarkdownViewer limit={250}>{description}</NoMarkdownViewer>
<NoMarkdownViewer
limit={descriptionExpanded ? undefined : 250}
shouldWrap={previewType === PreviewType.HOVER_CARD}
readMore={
previewType === PreviewType.HOVER_CARD ? (
<Typography.Link
onClickCapture={(e) => {
onPreventMouseDown(e);
setDescriptionExpanded(!descriptionExpanded);
}}
>
{descriptionExpanded ? 'Show Less' : 'Show More'}
</Typography.Link>
) : undefined
}
>
{description}
</NoMarkdownViewer>
</DescriptionContainer>
)}
{(domain || hasGlossaryTerms || hasTags) && (

View File

@ -1,10 +1,10 @@
import { Tooltip } from 'antd';
import React from 'react';
import { useHistory } from 'react-router';
import { Entity, SearchResult } from '../../../../types.generated';
import { Entity } from '../../../../types.generated';
import { IconStyleType } from '../../../entity/Entity';
import { useEntityRegistry } from '../../../useEntityRegistry';
import { EntityPreviewTag } from './EntityPreviewTag';
import { HoverEntityTooltip } from './HoverEntityTooltip';
type Props = {
entities: Array<Entity>;
@ -34,21 +34,7 @@ export const CompactEntityNameList = ({ entities, onClick, linkUrlParams, showTo
history.push(url);
}}
>
<Tooltip
visible={showTooltips ? undefined : false}
color="white"
placement="topRight"
overlayStyle={{ minWidth: 400 }}
overlayInnerStyle={{ padding: 12 }}
title={
<a href={url}>
{entityRegistry.renderSearchResult(entity.type, {
entity,
matchedFields: [],
} as SearchResult)}
</a>
}
>
<HoverEntityTooltip entity={entity} canOpen={showTooltips}>
<span data-testid={`compact-entity-link-${entity.urn}`}>
<EntityPreviewTag
displayName={displayName}
@ -61,7 +47,7 @@ export const CompactEntityNameList = ({ entities, onClick, linkUrlParams, showTo
onClick={() => onClick?.(index)}
/>
</span>
</Tooltip>
</HoverEntityTooltip>
</span>
);
})}

View File

@ -0,0 +1,34 @@
import { Tooltip } from 'antd';
import React from 'react';
import { Entity } from '../../../../types.generated';
import { PreviewType } from '../../../entity/Entity';
import { useEntityRegistry } from '../../../useEntityRegistry';
type Props = {
entity?: Entity;
// whether the tooltip can be opened or if it should always stay closed
canOpen?: boolean;
children: React.ReactNode;
};
export const HoverEntityTooltip = ({ entity, canOpen = true, children }: Props) => {
const entityRegistry = useEntityRegistry();
if (!entity || !entity.type || !entity.urn) {
return <>{children}</>;
}
const url = entityRegistry.getEntityUrl(entity.type, entity.urn);
return (
<Tooltip
visible={canOpen ? undefined : false}
color="white"
placement="topRight"
overlayStyle={{ minWidth: 500 }}
overlayInnerStyle={{ padding: 12 }}
title={<a href={url}>{entityRegistry.renderPreview(entity.type, PreviewType.HOVER_CARD, entity)}</a>}
>
{children}
</Tooltip>
);
};

View File

@ -163,7 +163,7 @@ export const SearchFilterLabel = ({ aggregation, field }: Props) => {
const truncatedDomainName = displayName.length > 25 ? `${displayName.slice(0, 25)}...` : displayName;
return (
<Tooltip title={displayName}>
<DomainLink urn={domain.urn} name={truncatedDomainName} />({countText})
<DomainLink domain={domain} name={truncatedDomainName} />({countText})
</Tooltip>
);
}

View File

@ -2,8 +2,9 @@ import { Tag } from 'antd';
import React from 'react';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { EntityType } from '../../../types.generated';
import { Domain, EntityType } from '../../../types.generated';
import { IconStyleType } from '../../entity/Entity';
import { HoverEntityTooltip } from '../../recommendations/renderer/component/HoverEntityTooltip';
import { useEntityRegistry } from '../../useEntityRegistry';
const DomainLinkContainer = styled(Link)`
@ -12,23 +13,28 @@ const DomainLinkContainer = styled(Link)`
`;
export type Props = {
urn: string;
name: string;
domain: Domain;
name?: string;
closable?: boolean;
onClose?: (e: any) => void;
tagStyle?: any | undefined;
};
export const DomainLink = ({ urn, name, closable, onClose, tagStyle }: Props): JSX.Element => {
export const DomainLink = ({ domain, name, closable, onClose, tagStyle }: Props): JSX.Element => {
const entityRegistry = useEntityRegistry();
const urn = domain?.urn;
const displayName = name || entityRegistry.getDisplayName(EntityType.Domain, domain);
return (
<DomainLinkContainer to={entityRegistry.getEntityUrl(EntityType.Domain, urn)}>
<Tag style={tagStyle} closable={closable} onClose={onClose}>
<span style={{ paddingRight: '4px' }}>
{entityRegistry.getIcon(EntityType.Domain, 10, IconStyleType.ACCENT)}
</span>
{name}
</Tag>
</DomainLinkContainer>
<HoverEntityTooltip entity={domain}>
<DomainLinkContainer to={entityRegistry.getEntityUrl(EntityType.Domain, urn)}>
<Tag style={tagStyle} closable={closable} onClose={onClose}>
<span style={{ paddingRight: '4px' }}>
{entityRegistry.getIcon(EntityType.Domain, 10, IconStyleType.ACCENT)}
</span>
{displayName}
</Tag>
</DomainLinkContainer>
</HoverEntityTooltip>
);
};

View File

@ -20,6 +20,7 @@ import { useRemoveTagMutation, useRemoveTermMutation } from '../../../graphql/mu
import { DomainLink } from './DomainLink';
import { TagProfileDrawer } from './TagProfileDrawer';
import EditTagTermsModal from './AddTagsTermsModal';
import { HoverEntityTooltip } from '../../recommendations/renderer/component/HoverEntityTooltip';
type Props = {
uneditableTags?: GlobalTags | null;
@ -180,7 +181,7 @@ export default function TagTermGroup({
return (
<>
{domain && (
<DomainLink urn={domain.urn} name={entityRegistry.getDisplayName(EntityType.Domain, domain) || ''} />
<DomainLink domain={domain} name={entityRegistry.getDisplayName(EntityType.Domain, domain) || ''} />
)}
{uneditableGlossaryTerms?.terms?.map((term) => {
renderedTags += 1;
@ -195,31 +196,38 @@ export default function TagTermGroup({
if (maxShow && renderedTags > maxShow) return null;
return (
<HoverEntityTooltip entity={term.term}>
<TermLink
to={entityRegistry.getEntityUrl(EntityType.GlossaryTerm, term.term.urn)}
key={term.term.urn}
>
<Tag closable={false} style={{ cursor: 'pointer' }}>
<BookOutlined style={{ marginRight: '3%' }} />
{entityRegistry.getDisplayName(EntityType.GlossaryTerm, term.term)}
</Tag>
</TermLink>
</HoverEntityTooltip>
);
})}
{editableGlossaryTerms?.terms?.map((term) => (
<HoverEntityTooltip entity={term.term}>
<TermLink
to={entityRegistry.getEntityUrl(EntityType.GlossaryTerm, term.term.urn)}
key={term.term.urn}
>
<Tag closable={false} style={{ cursor: 'pointer' }}>
<Tag
style={{ cursor: 'pointer' }}
closable={canRemove}
onClose={(e) => {
e.preventDefault();
removeTerm(term);
}}
>
<BookOutlined style={{ marginRight: '3%' }} />
{entityRegistry.getDisplayName(EntityType.GlossaryTerm, term.term)}
</Tag>
</TermLink>
);
})}
{editableGlossaryTerms?.terms?.map((term) => (
<TermLink to={entityRegistry.getEntityUrl(EntityType.GlossaryTerm, term.term.urn)} key={term.term.urn}>
<Tag
style={{ cursor: 'pointer' }}
closable={canRemove}
onClose={(e) => {
e.preventDefault();
removeTerm(term);
}}
>
<BookOutlined style={{ marginRight: '3%' }} />
{entityRegistry.getDisplayName(EntityType.GlossaryTerm, term.term)}
</Tag>
</TermLink>
</HoverEntityTooltip>
))}
{/* uneditable tags are provided by ingestion pipelines exclusively */}
{uneditableTags?.tags?.map((tag) => {
@ -231,17 +239,19 @@ export default function TagTermGroup({
if (maxShow && renderedTags > maxShow) return null;
return (
<TagLink key={tag?.tag?.urn}>
<StyledTag
style={{ cursor: 'pointer' }}
onClick={() => showTagProfileDrawer(tag?.tag?.urn)}
$colorHash={tag?.tag?.urn}
$color={tag?.tag?.properties?.colorHex}
closable={false}
>
{entityRegistry.getDisplayName(EntityType.Tag, tag.tag)}
</StyledTag>
</TagLink>
<HoverEntityTooltip entity={tag?.tag}>
<TagLink key={tag?.tag?.urn}>
<StyledTag
style={{ cursor: 'pointer' }}
onClick={() => showTagProfileDrawer(tag?.tag?.urn)}
$colorHash={tag?.tag?.urn}
$color={tag?.tag?.properties?.colorHex}
closable={false}
>
{entityRegistry.getDisplayName(EntityType.Tag, tag.tag)}
</StyledTag>
</TagLink>
</HoverEntityTooltip>
);
})}
{/* editable tags may be provided by ingestion pipelines or the UI */}
@ -249,21 +259,23 @@ export default function TagTermGroup({
renderedTags += 1;
if (maxShow && renderedTags > maxShow) return null;
return (
<TagLink>
<StyledTag
style={{ cursor: 'pointer' }}
onClick={() => showTagProfileDrawer(tag?.tag?.urn)}
$colorHash={tag?.tag?.urn}
$color={tag?.tag?.properties?.colorHex}
closable={canRemove}
onClose={(e) => {
e.preventDefault();
removeTag(tag);
}}
>
{tag?.tag?.name}
</StyledTag>
</TagLink>
<HoverEntityTooltip entity={tag?.tag}>
<TagLink>
<StyledTag
style={{ cursor: 'pointer' }}
onClick={() => showTagProfileDrawer(tag?.tag?.urn)}
$colorHash={tag?.tag?.urn}
$color={tag?.tag?.properties?.colorHex}
closable={canRemove}
onClose={(e) => {
e.preventDefault();
removeTag(tag);
}}
>
{tag?.tag?.name}
</StyledTag>
</TagLink>
</HoverEntityTooltip>
);
})}
{tagProfileDrawerVisible && (

View File

@ -91,6 +91,9 @@ query getChart($urn: String!) {
}
}
}
inputFields {
...inputFieldsFields
}
}
}

View File

@ -2,6 +2,7 @@ fragment globalTagsFields on GlobalTags {
tags {
tag {
urn
type
name
description
properties {
@ -30,8 +31,19 @@ fragment glossaryTerm on GlossaryTerm {
hierarchicalName
properties {
name
description
definition
termSource
customProperties {
key
value
}
}
ownership {
...ownershipFields
}
parentNodes {
...parentNodesFields
}
}
@ -411,6 +423,9 @@ fragment dashboardFields on Dashboard {
}
}
}
inputFields {
...inputFieldsFields
}
}
fragment nonRecursiveMLFeature on MLFeature {
@ -600,6 +615,24 @@ fragment nonRecursiveMLFeatureTable on MLFeatureTable {
}
}
fragment schemaFieldFields on SchemaField {
fieldPath
label
jsonPath
nullable
description
type
nativeDataType
recursive
isPartOfKey
globalTags {
...globalTagsFields
}
glossaryTerms {
...glossaryTerms
}
}
fragment schemaMetadataFields on SchemaMetadata {
aspectVersion
createdAt
@ -619,20 +652,7 @@ fragment schemaMetadataFields on SchemaMetadata {
}
}
fields {
fieldPath
jsonPath
nullable
description
type
nativeDataType
recursive
isPartOfKey
globalTags {
...globalTagsFields
}
glossaryTerms {
...glossaryTerms
}
...schemaFieldFields
}
primaryKeys
foreignKeys {
@ -839,9 +859,20 @@ fragment entityContainer on Container {
fragment entityDomain on DomainAssociation {
domain {
urn
type
properties {
name
description
}
}
associatedUrn
}
fragment inputFieldsFields on InputFields {
fields {
schemaFieldUrn
schemaField {
...schemaFieldFields
}
}
}

View File

@ -375,6 +375,9 @@ fragment searchResultFields on Entity {
}
}
}
inputFields {
...inputFieldsFields
}
}
... on Chart {
chartId
@ -437,6 +440,9 @@ fragment searchResultFields on Entity {
}
}
}
inputFields {
...inputFieldsFields
}
}
... on DataFlow {
flowId
@ -536,6 +542,9 @@ fragment searchResultFields on Entity {
deprecation {
...deprecationFields
}
parentNodes {
...parentNodesFields
}
}
... on GlossaryNode {
...glossaryNode

View File

@ -67,6 +67,7 @@ public class Constants {
public static final String OPERATION_ASPECT_NAME = "operation";
public static final String SIBLINGS_ASPECT_NAME = "siblings";
public static final String ORIGIN_ASPECT_NAME = "origin";
public static final String INPUT_FIELDS_ASPECT_NAME = "inputFields";
// User
public static final String CORP_USER_KEY_ASPECT_NAME = "corpUserKey";

View File

@ -1,152 +1,189 @@
describe('mutations', () => {
describe("mutations", () => {
before(() => {
// warm up elastic by issuing a `*` search
cy.login();
cy.visit('http://localhost:9002/search?query=%2A');
cy.visit("http://localhost:9002/search?query=%2A");
cy.wait(5000);
});
it('can create and add a tag to dataset and visit new tag page', () => {
cy.deleteUrn('urn:li:tag:CypressTestAddTag')
it("can create and add a tag to dataset and visit new tag page", () => {
cy.deleteUrn("urn:li:tag:CypressTestAddTag");
cy.login();
cy.visit('/dataset/urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)');
cy.contains('cypress_logging_events');
cy.visit(
"/dataset/urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)"
);
cy.contains("cypress_logging_events");
cy.contains('Add Tag').click({force:true});
cy.contains("Add Tag").click({ force: true });
cy.focused().type('CypressTestAddTag');
cy.focused().type("CypressTestAddTag");
cy.contains('Create CypressTestAddTag').click({force:true});
cy.contains("Create CypressTestAddTag").click({ force: true });
cy.get('textarea').type('CypressTestAddTag Test Description');
cy.get("textarea").type("CypressTestAddTag Test Description");
cy.contains(/Create$/).click({force:true});
cy.contains(/Create$/).click({ force: true });
// wait a breath for elasticsearch to index the tag being applied to the dataset- if we navigate too quick ES
// wont know and we'll see applied to 0 entities
cy.wait(2000);
// go to tag drawer
cy.contains('CypressTestAddTag').click();
cy.contains("CypressTestAddTag").click({ force: true });
cy.wait(1000);
// Click the Tag Details to launch full profile
cy.contains('Tag Details').click();
cy.contains("Tag Details").click({ force: true });
cy.wait(1000);
// title of tag page
cy.contains('CypressTestAddTag');
cy.contains("CypressTestAddTag");
// description of tag page
cy.contains('CypressTestAddTag Test Description');
cy.contains("CypressTestAddTag Test Description");
// used by panel - click to search
cy.contains('1 Datasets').click();
cy.contains("1 Datasets").click({ force: true });
// verify dataset shows up in search now
cy.contains('of 1 result').click();
cy.contains('cypress_logging_events').click();
cy.contains('CypressTestAddTag').within(() => cy.get('span[aria-label=close]').click());
cy.contains('Yes').click();
cy.contains("of 1 result").click({ force: true });
cy.contains("cypress_logging_events").click({ force: true });
cy.contains("CypressTestAddTag").within(() =>
cy.get("span[aria-label=close]").click()
);
cy.contains("Yes").click();
cy.contains('CypressTestAddTag').should('not.exist');
cy.contains("CypressTestAddTag").should("not.exist");
cy.deleteUrn('urn:li:tag:CypressTestAddTag')
cy.deleteUrn("urn:li:tag:CypressTestAddTag");
});
it('can add and remove terms from a dataset', () => {
it("can add and remove terms from a dataset", () => {
cy.login();
cy.visit('/dataset/urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)');
cy.contains('cypress_logging_events');
cy.visit(
"/dataset/urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)"
);
cy.contains("cypress_logging_events");
cy.contains('Add Term').click();
cy.contains("Add Term").click();
cy.focused().type('CypressTerm');
cy.focused().type("CypressTerm");
cy.get('.ant-select-item-option-content').within(() => cy.contains('CypressTerm').click({force: true}));
cy.get(".ant-select-item-option-content").within(() =>
cy.contains("CypressTerm").click({ force: true })
);
cy.get('[data-testid="add-tag-term-from-modal-btn"]').click({force: true});
cy.get('[data-testid="add-tag-term-from-modal-btn"]').should('not.exist');
cy.get('[data-testid="add-tag-term-from-modal-btn"]').click({
force: true,
});
cy.get('[data-testid="add-tag-term-from-modal-btn"]').should("not.exist");
cy.contains('CypressTerm');
cy.contains("CypressTerm");
cy.get('a[href="/glossaryTerm/urn:li:glossaryTerm:CypressNode.CypressTerm"]').within(() => cy.get('span[aria-label=close]').click());
cy.contains('Yes').click();
cy.get(
'a[href="/glossaryTerm/urn:li:glossaryTerm:CypressNode.CypressTerm"]'
).within(() => cy.get("span[aria-label=close]").click());
cy.contains("Yes").click();
cy.contains('CypressTerm').should('not.exist');
cy.contains("CypressTerm").should("not.exist");
});
it('can add and remove tags from a dataset field', () => {
cy.login();
cy.viewport(2000, 800)
cy.visit('/dataset/urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)');
cy.get('[data-testid="schema-field-event_name-tags"]').trigger('mouseover', {force: true});
cy.get('[data-testid="schema-field-event_name-tags"]').within(() => cy.contains('Add Tag').click())
it("can add and remove tags from a dataset field", () => {
cy.login();
cy.viewport(2000, 800);
cy.visit(
"/dataset/urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)"
);
cy.get('[data-testid="schema-field-event_name-tags"]').trigger(
"mouseover",
{ force: true }
);
cy.get('[data-testid="schema-field-event_name-tags"]').within(() =>
cy.contains("Add Tag").click()
);
cy.focused().type('CypressTestAddTag2');
cy.focused().type("CypressTestAddTag2");
cy.contains('Create CypressTestAddTag2').click({force:true});
cy.contains("Create CypressTestAddTag2").click({ force: true });
cy.get('textarea').type('CypressTestAddTag2 Test Description');
cy.get("textarea").type("CypressTestAddTag2 Test Description");
cy.contains(/Create$/).click({force:true});
cy.contains(/Create$/).click({ force: true });
// wait a breath for elasticsearch to index the tag being applied to the dataset- if we navigate too quick ES
// wont know and we'll see applied to 0 entities
cy.wait(2000);
// wait a breath for elasticsearch to index the tag being applied to the dataset- if we navigate too quick ES
// wont know and we'll see applied to 0 entities
cy.wait(2000);
// go to tag drawer
cy.contains('CypressTestAddTag2').click();
// go to tag drawer
cy.contains("CypressTestAddTag2").click({ force: true });
cy.wait(1000);
cy.wait(1000);
// Click the Tag Details to launch full profile
cy.contains('Tag Details').click();
// Click the Tag Details to launch full profile
cy.contains("Tag Details").click({ force: true });
cy.wait(1000);
cy.wait(1000);
// title of tag page
cy.contains('CypressTestAddTag2');
// title of tag page
cy.contains("CypressTestAddTag2");
// description of tag page
cy.contains('CypressTestAddTag2 Test Description');
// description of tag page
cy.contains("CypressTestAddTag2 Test Description");
// used by panel - click to search
cy.contains('1 Datasets').click();
// used by panel - click to search
cy.contains("1 Datasets").click();
// verify dataset shows up in search now
cy.contains('of 1 result').click();
cy.contains('cypress_logging_events').click();
cy.contains('CypressTestAddTag2').within(() => cy.get('span[aria-label=close]').trigger('mouseover', {force: true}).click());
cy.contains('Yes').click();
// verify dataset shows up in search now
cy.contains("of 1 result").click();
cy.contains("cypress_logging_events").click();
cy.contains("CypressTestAddTag2").within(() =>
cy
.get("span[aria-label=close]")
.trigger("mouseover", { force: true })
.click({ force: true })
);
cy.contains("Yes").click({ force: true });
cy.contains('CypressTestAddTag2').should('not.exist');
cy.contains("CypressTestAddTag2").should("not.exist");
cy.deleteUrn('urn:li:tag:CypressTestAddTag2')
});
cy.deleteUrn("urn:li:tag:CypressTestAddTag2");
});
it('can add and remove terms from a dataset field', () => {
it("can add and remove terms from a dataset field", () => {
cy.login();
// make space for the glossary term column
cy.viewport(2000, 800)
cy.visit('/dataset/urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)');
cy.get('[data-testid="schema-field-event_name-terms"]').trigger('mouseover', {force: true});
cy.get('[data-testid="schema-field-event_name-terms"]').within(() => cy.contains('Add Term').click())
cy.viewport(2000, 800);
cy.visit(
"/dataset/urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)"
);
cy.get('[data-testid="schema-field-event_name-terms"]').trigger(
"mouseover",
{ force: true }
);
cy.get('[data-testid="schema-field-event_name-terms"]').within(() =>
cy.contains("Add Term").click({ force: true })
);
cy.focused().type('CypressTerm');
cy.focused().type("CypressTerm");
cy.get('.ant-select-item-option-content').within(() => cy.contains('CypressTerm').click({force: true}));
cy.get(".ant-select-item-option-content").within(() =>
cy.contains("CypressTerm").click({ force: true })
);
cy.get('[data-testid="add-tag-term-from-modal-btn"]').click({force: true});
cy.get('[data-testid="add-tag-term-from-modal-btn"]').should('not.exist');
cy.get('[data-testid="add-tag-term-from-modal-btn"]').click({
force: true,
});
cy.get('[data-testid="add-tag-term-from-modal-btn"]').should("not.exist");
cy.contains('CypressTerm');
cy.contains("CypressTerm");
cy.get('a[href="/glossaryTerm/urn:li:glossaryTerm:CypressNode.CypressTerm"]').within(() => cy.get('span[aria-label=close]').click());
cy.contains('Yes').click();
cy.get(
'a[href="/glossaryTerm/urn:li:glossaryTerm:CypressNode.CypressTerm"]'
).within(() => cy.get("span[aria-label=close]").click({ force: true }));
cy.contains("Yes").click({ force: true });
cy.contains('CypressTerm').should('not.exist');
cy.contains("CypressTerm").should("not.exist");
});
})
});