mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-27 09:58:14 +00:00
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:
parent
ca29f8b679
commit
5bf5fc2d66
@ -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");
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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',
|
||||
};
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
50
datahub-web-react/src/app/entity/chart/ChartSnippet.tsx
Normal file
50
datahub-web-react/src/app/entity/chart/ChartSnippet.tsx
Normal 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;
|
||||
};
|
||||
@ -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} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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 = {
|
||||
|
||||
@ -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) && (
|
||||
|
||||
@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -91,6 +91,9 @@ query getChart($urn: String!) {
|
||||
}
|
||||
}
|
||||
}
|
||||
inputFields {
|
||||
...inputFieldsFields
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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");
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user