mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-24 16:38:19 +00:00
feat(containers) Get and display all parent containers in header and search (#4910)
Co-authored-by: Chris Collins <chriscollins@Chriss-MBP.lan>
This commit is contained in:
parent
78c3ca039e
commit
0c5f844e4f
@ -63,6 +63,7 @@ import com.linkedin.datahub.graphql.resolvers.auth.GetAccessTokenResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.browse.BrowsePathsResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.browse.BrowseResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.config.AppConfigResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.container.ParentContainersResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.container.ContainerEntitiesResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.dataset.DatasetHealthResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.deprecation.UpdateDeprecationResolver;
|
||||
@ -482,6 +483,7 @@ public class GmsGraphQLEngine {
|
||||
return container.getContainer() != null ? container.getContainer().getUrn() : null;
|
||||
})
|
||||
)
|
||||
.dataFetcher("parentContainers", new ParentContainersResolver(entityClient))
|
||||
.dataFetcher("dataPlatformInstance",
|
||||
new LoadableTypeResolver<>(dataPlatformInstanceType,
|
||||
(env) -> {
|
||||
@ -724,7 +726,8 @@ public class GmsGraphQLEngine {
|
||||
this.entityClient,
|
||||
"dataset",
|
||||
"subTypes"))
|
||||
.dataFetcher("runs", new EntityRunsResolver(entityClient)))
|
||||
.dataFetcher("runs", new EntityRunsResolver(entityClient))
|
||||
.dataFetcher("parentContainers", new ParentContainersResolver(entityClient)))
|
||||
.type("Owner", typeWiring -> typeWiring
|
||||
.dataFetcher("owner", new OwnerTypeResolver<>(ownerTypes,
|
||||
(env) -> ((Owner) env.getSource()).getOwner()))
|
||||
@ -854,6 +857,7 @@ public class GmsGraphQLEngine {
|
||||
return dashboard.getContainer() != null ? dashboard.getContainer().getUrn() : null;
|
||||
})
|
||||
)
|
||||
.dataFetcher("parentContainers", new ParentContainersResolver(entityClient))
|
||||
);
|
||||
builder.type("DashboardInfo", typeWiring -> typeWiring
|
||||
.dataFetcher("charts", new LoadableTypeBatchResolver<>(chartType,
|
||||
@ -893,6 +897,7 @@ public class GmsGraphQLEngine {
|
||||
return chart.getContainer() != null ? chart.getContainer().getUrn() : null;
|
||||
})
|
||||
)
|
||||
.dataFetcher("parentContainers", new ParentContainersResolver(entityClient))
|
||||
);
|
||||
builder.type("ChartInfo", typeWiring -> typeWiring
|
||||
.dataFetcher("inputs", new LoadableTypeBatchResolver<>(datasetType,
|
||||
|
||||
@ -0,0 +1,76 @@
|
||||
package com.linkedin.datahub.graphql.resolvers.container;
|
||||
|
||||
import com.linkedin.common.urn.Urn;
|
||||
import com.linkedin.data.DataMap;
|
||||
import com.linkedin.datahub.graphql.QueryContext;
|
||||
import com.linkedin.datahub.graphql.exception.DataHubGraphQLException;
|
||||
import com.linkedin.datahub.graphql.generated.Container;
|
||||
import com.linkedin.datahub.graphql.generated.Entity;
|
||||
import com.linkedin.datahub.graphql.generated.ParentContainersResult;
|
||||
import com.linkedin.datahub.graphql.types.container.mappers.ContainerMapper;
|
||||
import com.linkedin.entity.EntityResponse;
|
||||
import com.linkedin.entity.client.EntityClient;
|
||||
import graphql.schema.DataFetcher;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static com.linkedin.metadata.Constants.CONTAINER_ASPECT_NAME;
|
||||
|
||||
public class ParentContainersResolver implements DataFetcher<CompletableFuture<ParentContainersResult>> {
|
||||
|
||||
private final EntityClient _entityClient;
|
||||
|
||||
public ParentContainersResolver(final EntityClient entityClient) {
|
||||
_entityClient = entityClient;
|
||||
}
|
||||
|
||||
private void aggregateParentContainers(List<Container> containers, String urn, QueryContext context) {
|
||||
try {
|
||||
Urn entityUrn = new Urn(urn);
|
||||
EntityResponse entityResponse = _entityClient.getV2(
|
||||
entityUrn.getEntityType(),
|
||||
entityUrn,
|
||||
Collections.singleton(CONTAINER_ASPECT_NAME),
|
||||
context.getAuthentication()
|
||||
);
|
||||
|
||||
if (entityResponse != null && entityResponse.getAspects().containsKey(CONTAINER_ASPECT_NAME)) {
|
||||
DataMap dataMap = entityResponse.getAspects().get(CONTAINER_ASPECT_NAME).getValue().data();
|
||||
com.linkedin.container.Container container = new com.linkedin.container.Container(dataMap);
|
||||
Urn containerUrn = container.getContainer();
|
||||
EntityResponse response = _entityClient.getV2(containerUrn.getEntityType(), containerUrn, null, context.getAuthentication());
|
||||
if (response != null) {
|
||||
Container mappedContainer = ContainerMapper.map(response);
|
||||
containers.add(mappedContainer);
|
||||
aggregateParentContainers(containers, mappedContainer.getUrn(), context);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ParentContainersResult> get(DataFetchingEnvironment environment) {
|
||||
|
||||
final QueryContext context = environment.getContext();
|
||||
final String urn = ((Entity) environment.getSource()).getUrn();
|
||||
final List<Container> containers = new ArrayList<>();
|
||||
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
aggregateParentContainers(containers, urn, context);
|
||||
final ParentContainersResult result = new ParentContainersResult();
|
||||
result.setCount(containers.size());
|
||||
result.setContainers(containers);
|
||||
return result;
|
||||
} catch (DataHubGraphQLException e) {
|
||||
throw new RuntimeException("Failed to load all containers", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -710,6 +710,11 @@ type Dataset implements EntityWithRelationships & Entity {
|
||||
"""
|
||||
container: Container
|
||||
|
||||
"""
|
||||
Recursively get the lineage of containers for this entity
|
||||
"""
|
||||
parentContainers: ParentContainersResult
|
||||
|
||||
"""
|
||||
Unique guid for dataset
|
||||
No longer to be used as the Dataset display name. Use properties.name instead
|
||||
@ -878,6 +883,21 @@ type Dataset implements EntityWithRelationships & Entity {
|
||||
runs(start: Int, count: Int, direction: RelationshipDirection!): DataProcessInstanceResult
|
||||
}
|
||||
|
||||
"""
|
||||
All of the parent containers for a given entity
|
||||
"""
|
||||
type ParentContainersResult {
|
||||
"""
|
||||
The number of containers bubbling up for this entity
|
||||
"""
|
||||
count: Int!
|
||||
|
||||
"""
|
||||
A list of parent containers in order from direct parent, to parent's parent etc. If there are no containers, return an emty list
|
||||
"""
|
||||
containers: [Container!]!
|
||||
}
|
||||
|
||||
"""
|
||||
A Dataset entity, which encompasses Relational Tables, Document store collections, streaming topics, and other sets of data having an independent lifecycle
|
||||
"""
|
||||
@ -902,6 +922,11 @@ type VersionedDataset implements Entity {
|
||||
"""
|
||||
container: Container
|
||||
|
||||
"""
|
||||
Recursively get the lineage of containers for this entity
|
||||
"""
|
||||
parentContainers: ParentContainersResult
|
||||
|
||||
"""
|
||||
Unique guid for dataset
|
||||
No longer to be used as the Dataset display name. Use properties.name instead
|
||||
@ -1515,6 +1540,11 @@ type Container implements Entity {
|
||||
"""
|
||||
container: Container
|
||||
|
||||
"""
|
||||
Recursively get the lineage of containers for this entity
|
||||
"""
|
||||
parentContainers: ParentContainersResult
|
||||
|
||||
"""
|
||||
Read-only properties that originate in the source data platform
|
||||
"""
|
||||
@ -3720,6 +3750,11 @@ type Dashboard implements EntityWithRelationships & Entity {
|
||||
"""
|
||||
container: Container
|
||||
|
||||
"""
|
||||
Recursively get the lineage of containers for this entity
|
||||
"""
|
||||
parentContainers: ParentContainersResult
|
||||
|
||||
"""
|
||||
The dashboard tool name
|
||||
Note that this will soon be deprecated in favor of a standardized notion of Data Platform
|
||||
@ -3973,6 +4008,11 @@ type Chart implements EntityWithRelationships & Entity {
|
||||
"""
|
||||
container: Container
|
||||
|
||||
"""
|
||||
Recursively get the lineage of containers for this entity
|
||||
"""
|
||||
parentContainers: ParentContainersResult
|
||||
|
||||
"""
|
||||
The chart tool name
|
||||
Note that this field will soon be deprecated in favor a unified notion of Data Platform
|
||||
|
||||
@ -0,0 +1,118 @@
|
||||
package com.linkedin.datahub.graphql.resolvers.container;
|
||||
|
||||
import com.datahub.authentication.Authentication;
|
||||
import com.linkedin.common.urn.Urn;
|
||||
import com.linkedin.container.Container;
|
||||
import com.linkedin.container.ContainerProperties;
|
||||
import com.linkedin.datahub.graphql.QueryContext;
|
||||
import com.linkedin.datahub.graphql.generated.Dataset;
|
||||
import com.linkedin.datahub.graphql.generated.EntityType;
|
||||
import com.linkedin.datahub.graphql.generated.ParentContainersResult;
|
||||
import com.linkedin.entity.Aspect;
|
||||
import com.linkedin.entity.EntityResponse;
|
||||
import com.linkedin.entity.EnvelopedAspect;
|
||||
import com.linkedin.entity.EnvelopedAspectMap;
|
||||
import com.linkedin.entity.client.EntityClient;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import org.mockito.Mockito;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.linkedin.metadata.Constants.CONTAINER_ASPECT_NAME;
|
||||
import static com.linkedin.metadata.Constants.CONTAINER_ENTITY_NAME;
|
||||
import static com.linkedin.metadata.Constants.CONTAINER_PROPERTIES_ASPECT_NAME;
|
||||
|
||||
import static org.testng.Assert.*;
|
||||
|
||||
public class ParentContainersResolverTest {
|
||||
@Test
|
||||
public void testGetSuccess() throws Exception {
|
||||
EntityClient mockClient = Mockito.mock(EntityClient.class);
|
||||
QueryContext mockContext = Mockito.mock(QueryContext.class);
|
||||
Mockito.when(mockContext.getAuthentication()).thenReturn(Mockito.mock(Authentication.class));
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
Urn datasetUrn = Urn.createFromString("urn:li:dataset:(test,test,test)");
|
||||
Dataset datasetEntity = new Dataset();
|
||||
datasetEntity.setUrn(datasetUrn.toString());
|
||||
datasetEntity.setType(EntityType.DATASET);
|
||||
Mockito.when(mockEnv.getSource()).thenReturn(datasetEntity);
|
||||
|
||||
final Container parentContainer1 = new Container().setContainer(Urn.createFromString("urn:li:container:test-container"));
|
||||
final Container parentContainer2 = new Container().setContainer(Urn.createFromString("urn:li:container:test-container2"));
|
||||
|
||||
Map<String, EnvelopedAspect> datasetAspects = new HashMap<>();
|
||||
datasetAspects.put(CONTAINER_ASPECT_NAME, new EnvelopedAspect().setValue(new Aspect(parentContainer1.data())));
|
||||
|
||||
Map<String, EnvelopedAspect> parentContainer1Aspects = new HashMap<>();
|
||||
parentContainer1Aspects.put(CONTAINER_PROPERTIES_ASPECT_NAME, new EnvelopedAspect().setValue(new Aspect(
|
||||
new ContainerProperties().setName("test_schema").data()
|
||||
)));
|
||||
parentContainer1Aspects.put(CONTAINER_ASPECT_NAME, new EnvelopedAspect().setValue(new Aspect(
|
||||
parentContainer2.data()
|
||||
)));
|
||||
|
||||
Map<String, EnvelopedAspect> parentContainer2Aspects = new HashMap<>();
|
||||
parentContainer2Aspects.put(CONTAINER_PROPERTIES_ASPECT_NAME, new EnvelopedAspect().setValue(new Aspect(
|
||||
new ContainerProperties().setName("test_database").data()
|
||||
)));
|
||||
|
||||
Mockito.when(mockClient.getV2(
|
||||
Mockito.eq(datasetUrn.getEntityType()),
|
||||
Mockito.eq(datasetUrn),
|
||||
Mockito.eq(Collections.singleton(CONTAINER_ASPECT_NAME)),
|
||||
Mockito.any(Authentication.class)
|
||||
)).thenReturn(new EntityResponse().setAspects(new EnvelopedAspectMap(datasetAspects)));
|
||||
|
||||
Mockito.when(mockClient.getV2(
|
||||
Mockito.eq(parentContainer1.getContainer().getEntityType()),
|
||||
Mockito.eq(parentContainer1.getContainer()),
|
||||
Mockito.eq(null),
|
||||
Mockito.any(Authentication.class)
|
||||
)).thenReturn(new EntityResponse()
|
||||
.setEntityName(CONTAINER_ENTITY_NAME)
|
||||
.setUrn(parentContainer1.getContainer())
|
||||
.setAspects(new EnvelopedAspectMap(parentContainer1Aspects)));
|
||||
|
||||
Mockito.when(mockClient.getV2(
|
||||
Mockito.eq(parentContainer1.getContainer().getEntityType()),
|
||||
Mockito.eq(parentContainer1.getContainer()),
|
||||
Mockito.eq(Collections.singleton(CONTAINER_ASPECT_NAME)),
|
||||
Mockito.any(Authentication.class)
|
||||
)).thenReturn(new EntityResponse().setAspects(new EnvelopedAspectMap(parentContainer1Aspects)));
|
||||
|
||||
Mockito.when(mockClient.getV2(
|
||||
Mockito.eq(parentContainer2.getContainer().getEntityType()),
|
||||
Mockito.eq(parentContainer2.getContainer()),
|
||||
Mockito.eq(null),
|
||||
Mockito.any(Authentication.class)
|
||||
)).thenReturn(new EntityResponse()
|
||||
.setEntityName(CONTAINER_ENTITY_NAME)
|
||||
.setUrn(parentContainer2.getContainer())
|
||||
.setAspects(new EnvelopedAspectMap(parentContainer2Aspects)));
|
||||
|
||||
Mockito.when(mockClient.getV2(
|
||||
Mockito.eq(parentContainer2.getContainer().getEntityType()),
|
||||
Mockito.eq(parentContainer2.getContainer()),
|
||||
Mockito.eq(Collections.singleton(CONTAINER_ASPECT_NAME)),
|
||||
Mockito.any(Authentication.class)
|
||||
)).thenReturn(new EntityResponse().setAspects(new EnvelopedAspectMap(parentContainer2Aspects)));
|
||||
|
||||
ParentContainersResolver resolver = new ParentContainersResolver(mockClient);
|
||||
ParentContainersResult result = resolver.get(mockEnv).get();
|
||||
|
||||
Mockito.verify(mockClient, Mockito.times(5)).getV2(
|
||||
Mockito.any(),
|
||||
Mockito.any(),
|
||||
Mockito.any(),
|
||||
Mockito.any()
|
||||
);
|
||||
assertEquals(result.getCount(), 2);
|
||||
assertEquals(result.getContainers().get(0).getUrn(), parentContainer1.getContainer().toString());
|
||||
assertEquals(result.getContainers().get(1).getUrn(), parentContainer2.getContainer().toString());
|
||||
}
|
||||
}
|
||||
@ -24,6 +24,7 @@ import {
|
||||
ScenarioType,
|
||||
RecommendationRenderType,
|
||||
RelationshipDirection,
|
||||
Container,
|
||||
} from './types.generated';
|
||||
import { GetTagDocument } from './graphql/tag.generated';
|
||||
import { GetMlModelDocument } from './graphql/mlModel.generated';
|
||||
@ -318,6 +319,10 @@ export const dataset3 = {
|
||||
customProperties: [{ key: 'propertyAKey', value: 'propertyAValue' }],
|
||||
externalUrl: 'https://data.hub',
|
||||
},
|
||||
parentContainers: {
|
||||
count: 0,
|
||||
containers: [],
|
||||
},
|
||||
editableProperties: null,
|
||||
created: {
|
||||
time: 0,
|
||||
@ -758,6 +763,29 @@ export const dataset7WithSelfReferentialLineage = {
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const container1 = {
|
||||
urn: 'urn:li:container:DATABASE',
|
||||
type: EntityType.Container,
|
||||
platform: dataPlatform,
|
||||
properties: {
|
||||
name: 'database1',
|
||||
__typename: 'ContainerProperties',
|
||||
},
|
||||
__typename: 'Container',
|
||||
} as Container;
|
||||
|
||||
export const container2 = {
|
||||
urn: 'urn:li:container:SCHEMA',
|
||||
type: EntityType.Container,
|
||||
platform: dataPlatform,
|
||||
properties: {
|
||||
name: 'schema1',
|
||||
__typename: 'ContainerProperties',
|
||||
},
|
||||
__typename: 'Container',
|
||||
} as Container;
|
||||
|
||||
const glossaryTerm1 = {
|
||||
urn: 'urn:li:glossaryTerm:1',
|
||||
type: EntityType.GlossaryTerm,
|
||||
|
||||
@ -152,6 +152,7 @@ export class ChartEntity implements Entity<Chart> {
|
||||
glossaryTerms={data?.glossaryTerms}
|
||||
logoUrl={data?.platform?.properties?.logoUrl}
|
||||
domain={data.domain}
|
||||
parentContainers={data.parentContainers}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
GlossaryTerms,
|
||||
Owner,
|
||||
SearchInsight,
|
||||
ParentContainersResult,
|
||||
} from '../../../../types.generated';
|
||||
import DefaultPreviewCard from '../../../preview/DefaultPreviewCard';
|
||||
import { useEntityRegistry } from '../../../useEntityRegistry';
|
||||
@ -27,6 +28,7 @@ export const ChartPreview = ({
|
||||
container,
|
||||
insights,
|
||||
logoUrl,
|
||||
parentContainers,
|
||||
}: {
|
||||
urn: string;
|
||||
platform: string;
|
||||
@ -41,6 +43,7 @@ export const ChartPreview = ({
|
||||
container?: Container | null;
|
||||
insights?: Array<SearchInsight> | null;
|
||||
logoUrl?: string | null;
|
||||
parentContainers?: ParentContainersResult | null;
|
||||
}): JSX.Element => {
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const capitalizedPlatform = capitalizeFirstLetter(platform);
|
||||
@ -61,6 +64,7 @@ export const ChartPreview = ({
|
||||
domain={domain}
|
||||
container={container || undefined}
|
||||
insights={insights}
|
||||
parentContainers={parentContainers}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -137,6 +137,7 @@ export class ContainerEntity implements Entity<Container> {
|
||||
container={data.container}
|
||||
entityCount={data.entities?.total}
|
||||
domain={data.domain}
|
||||
parentContainers={data.parentContainers}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,5 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Container, EntityType, Owner, SearchInsight, SubTypes, Domain } from '../../../../types.generated';
|
||||
import {
|
||||
Container,
|
||||
EntityType,
|
||||
Owner,
|
||||
SearchInsight,
|
||||
SubTypes,
|
||||
Domain,
|
||||
ParentContainersResult,
|
||||
} from '../../../../types.generated';
|
||||
import DefaultPreviewCard from '../../../preview/DefaultPreviewCard';
|
||||
import { useEntityRegistry } from '../../../useEntityRegistry';
|
||||
import { IconStyleType } from '../../Entity';
|
||||
@ -18,6 +26,7 @@ export const Preview = ({
|
||||
container,
|
||||
entityCount,
|
||||
domain,
|
||||
parentContainers,
|
||||
}: {
|
||||
urn: string;
|
||||
name: string;
|
||||
@ -32,6 +41,7 @@ export const Preview = ({
|
||||
container?: Container | null;
|
||||
entityCount?: number;
|
||||
domain?: Domain | null;
|
||||
parentContainers?: ParentContainersResult | null;
|
||||
}): JSX.Element => {
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const typeName = (subTypes?.typeNames?.length && subTypes?.typeNames[0]) || 'Container';
|
||||
@ -51,6 +61,7 @@ export const Preview = ({
|
||||
typeIcon={entityRegistry.getIcon(EntityType.Container, 12, IconStyleType.ACCENT)}
|
||||
entityCount={entityCount}
|
||||
domain={domain || undefined}
|
||||
parentContainers={parentContainers}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -172,6 +172,7 @@ export class DashboardEntity implements Entity<Dashboard> {
|
||||
logoUrl={data?.platform?.properties?.logoUrl || ''}
|
||||
domain={data.domain}
|
||||
container={data.container}
|
||||
parentContainers={data.parentContainers}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
GlossaryTerms,
|
||||
Owner,
|
||||
SearchInsight,
|
||||
ParentContainersResult,
|
||||
} from '../../../../types.generated';
|
||||
import DefaultPreviewCard from '../../../preview/DefaultPreviewCard';
|
||||
import { useEntityRegistry } from '../../../useEntityRegistry';
|
||||
@ -27,6 +28,7 @@ export const DashboardPreview = ({
|
||||
container,
|
||||
insights,
|
||||
logoUrl,
|
||||
parentContainers,
|
||||
}: {
|
||||
urn: string;
|
||||
platform: string;
|
||||
@ -41,6 +43,7 @@ export const DashboardPreview = ({
|
||||
container?: Container | null;
|
||||
insights?: Array<SearchInsight> | null;
|
||||
logoUrl?: string | null;
|
||||
parentContainers?: ParentContainersResult | null;
|
||||
}): JSX.Element => {
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const capitalizedPlatform = capitalizeFirstLetter(platform);
|
||||
@ -61,6 +64,7 @@ export const DashboardPreview = ({
|
||||
glossaryTerms={glossaryTerms || undefined}
|
||||
domain={domain}
|
||||
insights={insights}
|
||||
parentContainers={parentContainers}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -261,6 +261,7 @@ export class DatasetEntity implements Entity<Dataset> {
|
||||
glossaryTerms={data.glossaryTerms}
|
||||
subtype={data.subTypes?.typeNames?.[0]}
|
||||
container={data.container}
|
||||
parentContainers={data.parentContainers}
|
||||
snippet={
|
||||
// Add match highlights only if all the matched fields are in the FIELDS_TO_HIGHLIGHT
|
||||
result.matchedFields.length > 0 &&
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
SearchInsight,
|
||||
Domain,
|
||||
Container,
|
||||
ParentContainersResult,
|
||||
} from '../../../../types.generated';
|
||||
import DefaultPreviewCard from '../../../preview/DefaultPreviewCard';
|
||||
import { useEntityRegistry } from '../../../useEntityRegistry';
|
||||
@ -30,6 +31,7 @@ export const Preview = ({
|
||||
glossaryTerms,
|
||||
subtype,
|
||||
container,
|
||||
parentContainers,
|
||||
}: {
|
||||
urn: string;
|
||||
name: string;
|
||||
@ -46,6 +48,7 @@ export const Preview = ({
|
||||
glossaryTerms?: GlossaryTerms | null;
|
||||
subtype?: string | null;
|
||||
container?: Container | null;
|
||||
parentContainers?: ParentContainersResult | null;
|
||||
}): JSX.Element => {
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const capitalPlatformName = capitalizeFirstLetterOnly(platformName);
|
||||
@ -67,6 +70,7 @@ export const Preview = ({
|
||||
snippet={snippet}
|
||||
glossaryTerms={glossaryTerms || undefined}
|
||||
insights={insights}
|
||||
parentContainers={parentContainers}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,134 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import PlatformContentView, { getParentContainerNames } from '../header/PlatformContent/PlatformContentView';
|
||||
import { EntityType } from '../../../../../../types.generated';
|
||||
import { container1, container2 } from '../../../../../../Mocks';
|
||||
|
||||
jest.mock('../../../../../useEntityRegistry', () => ({
|
||||
useEntityRegistry: () => ({
|
||||
getEntityUrl: jest.fn(() => 'test'),
|
||||
getDisplayName: jest.fn(() => 'database1'),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('PlatformContent', () => {
|
||||
const defaultProps = {
|
||||
platformName: 'mysql',
|
||||
entityLogoComponent: <></>,
|
||||
typeIcon: <></>,
|
||||
entityType: EntityType.Dataset,
|
||||
parentContainers: [],
|
||||
parentContainersRef: React.createRef<HTMLDivElement>(),
|
||||
areContainersTruncated: false,
|
||||
};
|
||||
|
||||
it('should not render any containers if there are none in parentContainers', () => {
|
||||
const { queryAllByTestId } = render(<PlatformContentView {...defaultProps} />);
|
||||
|
||||
expect(queryAllByTestId('container')).toHaveLength(0);
|
||||
expect(queryAllByTestId('right-arrow')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should render a direct parent container correctly when there is only one container', () => {
|
||||
const { queryAllByTestId, getByText } = render(
|
||||
<BrowserRouter>
|
||||
<PlatformContentView {...defaultProps} parentContainers={[container1]} />
|
||||
</BrowserRouter>,
|
||||
);
|
||||
|
||||
expect(queryAllByTestId('container')).toHaveLength(1);
|
||||
expect(queryAllByTestId('right-arrow')).toHaveLength(1);
|
||||
expect(getByText('database1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render all parent containers properly', () => {
|
||||
const { queryAllByTestId } = render(
|
||||
<BrowserRouter>
|
||||
<PlatformContentView {...defaultProps} parentContainers={[container1, container2]} />
|
||||
</BrowserRouter>,
|
||||
);
|
||||
|
||||
expect(queryAllByTestId('container')).toHaveLength(2);
|
||||
expect(queryAllByTestId('right-arrow')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should render the correct number of right arrows with no containers and an instanceId', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<PlatformContentView {...defaultProps} instanceId="mysql1" />
|
||||
</BrowserRouter>,
|
||||
);
|
||||
expect(screen.queryAllByTestId('right-arrow')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should render the correct number of right arrows with multiple containers and an instanceId', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<PlatformContentView
|
||||
{...defaultProps}
|
||||
instanceId="mysql1"
|
||||
parentContainers={[container1, container2]}
|
||||
/>
|
||||
</BrowserRouter>,
|
||||
);
|
||||
expect(screen.queryAllByTestId('right-arrow')).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should render the correct number of right arrows with one containers and an instanceId', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<PlatformContentView {...defaultProps} instanceId="mysql1" parentContainers={[container2]} />
|
||||
</BrowserRouter>,
|
||||
);
|
||||
expect(screen.queryAllByTestId('right-arrow')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should render the correct number of right arrows with multiple containers and no instanceId', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<PlatformContentView {...defaultProps} parentContainers={[container1, container2]} />
|
||||
</BrowserRouter>,
|
||||
);
|
||||
expect(screen.queryAllByTestId('right-arrow')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should render the correct number of right arrows with one container and no instanceId', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<PlatformContentView {...defaultProps} parentContainers={[container1]} />
|
||||
</BrowserRouter>,
|
||||
);
|
||||
expect(screen.queryAllByTestId('right-arrow')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should render the correct number of right arrows with no containers and no instanceID', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<PlatformContentView {...defaultProps} parentContainers={[]} />
|
||||
</BrowserRouter>,
|
||||
);
|
||||
expect(screen.queryAllByTestId('right-arrow')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getParentContainerNames', () => {
|
||||
it('should return an empty string if there are no parent containers', () => {
|
||||
const parentContainerNames = getParentContainerNames([]);
|
||||
expect(parentContainerNames).toBe('');
|
||||
});
|
||||
|
||||
it('should return the name of the parent container if there is only one', () => {
|
||||
const parentContainerNames = getParentContainerNames([container1]);
|
||||
expect(parentContainerNames).toBe(container1.properties?.name);
|
||||
});
|
||||
|
||||
it('should return the names of the parents in reverse order, separated by cright arrows if there are multiple without manipulating the original array', () => {
|
||||
const parentContainers = [container1, container2];
|
||||
const parentContainersCopy = [...parentContainers];
|
||||
const parentContainerNames = getParentContainerNames(parentContainers);
|
||||
|
||||
expect(parentContainerNames).toBe(`${container2.properties?.name} > ${container1.properties?.name}`);
|
||||
expect(parentContainers).toMatchObject(parentContainersCopy);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { Typography } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
import { ANTD_GRAY } from '../../../constants';
|
||||
|
||||
export const EntityCountText = styled(Typography.Text)`
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
font-weight: 400;
|
||||
color: ${ANTD_GRAY[7]};
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
entityCount?: number;
|
||||
}
|
||||
|
||||
function EntityCount(props: Props) {
|
||||
const { entityCount } = props;
|
||||
|
||||
if (!entityCount || entityCount <= 0) return null;
|
||||
|
||||
return (
|
||||
<EntityCountText className="entityCount">
|
||||
{entityCount.toLocaleString()} {entityCount === 1 ? 'entity' : 'entities'}
|
||||
</EntityCountText>
|
||||
);
|
||||
}
|
||||
|
||||
export default EntityCount;
|
||||
@ -3,21 +3,16 @@ import {
|
||||
CheckOutlined,
|
||||
CopyOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
FolderOpenOutlined,
|
||||
InfoCircleOutlined,
|
||||
LinkOutlined,
|
||||
MoreOutlined,
|
||||
RightOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Typography, Image, Button, Tooltip, Menu, Dropdown, message, Popover } from 'antd';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Typography, Button, Tooltip, Menu, Dropdown, message, Popover } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
import moment from 'moment';
|
||||
|
||||
import { EntityType } from '../../../../../../types.generated';
|
||||
import { capitalizeFirstLetterOnly } from '../../../../../shared/textUtil';
|
||||
import { useEntityRegistry } from '../../../../../useEntityRegistry';
|
||||
import { IconStyleType } from '../../../../Entity';
|
||||
import { ANTD_GRAY } from '../../../constants';
|
||||
import { useEntityData, useRefetch } from '../../../EntityContext';
|
||||
import analytics, { EventType, EntityActionType } from '../../../../../analytics';
|
||||
@ -25,17 +20,9 @@ import { EntityHealthStatus } from './EntityHealthStatus';
|
||||
import { useUpdateDeprecationMutation } from '../../../../../../graphql/mutations.generated';
|
||||
import { getLocaleTimezone } from '../../../../../shared/time/timeUtils';
|
||||
import { AddDeprecationDetailsModal } from './AddDeprecationDetailsModal';
|
||||
|
||||
const LogoContainer = styled.span`
|
||||
margin-right: 10px;
|
||||
`;
|
||||
|
||||
const PreviewImage = styled(Image)`
|
||||
max-height: 17px;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
background-color: transparent;
|
||||
`;
|
||||
import PlatformContent from './PlatformContent';
|
||||
import { getPlatformName } from '../../../utils';
|
||||
import EntityCount from './EntityCount';
|
||||
|
||||
const EntityTitle = styled(Typography.Title)`
|
||||
&&& {
|
||||
@ -44,36 +31,6 @@ const EntityTitle = styled(Typography.Title)`
|
||||
}
|
||||
`;
|
||||
|
||||
const PlatformContent = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
const PlatformText = styled(Typography.Text)`
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
font-weight: 700;
|
||||
color: ${ANTD_GRAY[7]};
|
||||
`;
|
||||
|
||||
const EntityCountText = styled(Typography.Text)`
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
font-weight: 400;
|
||||
color: ${ANTD_GRAY[7]};
|
||||
`;
|
||||
|
||||
const PlatformDivider = styled.div`
|
||||
display: inline-block;
|
||||
padding-left: 10px;
|
||||
margin-right: 10px;
|
||||
border-right: 1px solid ${ANTD_GRAY[4]};
|
||||
height: 18px;
|
||||
vertical-align: text-top;
|
||||
`;
|
||||
|
||||
const HeaderContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -83,23 +40,10 @@ const HeaderContainer = styled.div`
|
||||
|
||||
const MainHeaderContent = styled.div`
|
||||
flex: 1;
|
||||
`;
|
||||
width: 85%;
|
||||
|
||||
const TypeIcon = styled.span`
|
||||
margin-right: 8px;
|
||||
`;
|
||||
|
||||
const ContainerText = styled(Typography.Text)`
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
font-weight: 400;
|
||||
color: ${ANTD_GRAY[9]};
|
||||
`;
|
||||
|
||||
const ContainerIcon = styled(FolderOpenOutlined)`
|
||||
&&& {
|
||||
font-size: 12px;
|
||||
margin-right: 4px;
|
||||
.entityCount {
|
||||
margin: 5px 0 -4px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
@ -171,17 +115,11 @@ export const EntityHeader = ({ showDeprecateOption }: Props) => {
|
||||
const [updateDeprecation] = useUpdateDeprecationMutation();
|
||||
const [showAddDeprecationDetailsModal, setShowAddDeprecationDetailsModal] = useState(false);
|
||||
const refetch = useRefetch();
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const [copiedUrn, setCopiedUrn] = useState(false);
|
||||
const basePlatformName = entityData?.platform?.properties?.displayName || entityData?.platform?.name;
|
||||
const basePlatformName = getPlatformName(entityData);
|
||||
const platformName = capitalizeFirstLetterOnly(basePlatformName);
|
||||
const platformLogoUrl = entityData?.platform?.properties?.logoUrl;
|
||||
const platformInstanceId = entityData?.dataPlatformInstance?.instanceId;
|
||||
const entityLogoComponent = entityRegistry.getIcon(entityType, 12, IconStyleType.ACCENT);
|
||||
const entityTypeCased =
|
||||
(entityData?.subTypes?.typeNames?.length && capitalizeFirstLetterOnly(entityData?.subTypes.typeNames[0])) ||
|
||||
entityRegistry.getEntityName(entityType);
|
||||
const externalUrl = entityData?.externalUrl || undefined;
|
||||
const entityCount = entityData?.entityCount;
|
||||
const hasExternalUrl = !!externalUrl;
|
||||
|
||||
const sendAnalytics = () => {
|
||||
@ -193,10 +131,6 @@ export const EntityHeader = ({ showDeprecateOption }: Props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const entityCount = entityData?.entityCount;
|
||||
const typeIcon = entityRegistry.getIcon(entityType, 12, IconStyleType.ACCENT);
|
||||
const container = entityData?.container;
|
||||
|
||||
// Update the Deprecation
|
||||
const handleUpdateDeprecation = async (deprecatedStatus: boolean) => {
|
||||
message.loading({ content: 'Updating...' });
|
||||
@ -247,42 +181,7 @@ export const EntityHeader = ({ showDeprecateOption }: Props) => {
|
||||
<>
|
||||
<HeaderContainer>
|
||||
<MainHeaderContent>
|
||||
<PlatformContent>
|
||||
{platformName && (
|
||||
<LogoContainer>
|
||||
{(!!platformLogoUrl && (
|
||||
<PreviewImage preview={false} src={platformLogoUrl} alt={platformName} />
|
||||
)) ||
|
||||
entityLogoComponent}
|
||||
</LogoContainer>
|
||||
)}
|
||||
<PlatformText>
|
||||
{platformName}
|
||||
{platformInstanceId && ` - ${platformInstanceId}`}
|
||||
</PlatformText>
|
||||
{(platformLogoUrl || platformName) && <PlatformDivider />}
|
||||
{typeIcon && <TypeIcon>{typeIcon}</TypeIcon>}
|
||||
<PlatformText>{entityData?.entityTypeOverride || entityTypeCased}</PlatformText>
|
||||
{container && (
|
||||
<Link to={entityRegistry.getEntityUrl(EntityType.Container, container?.urn)}>
|
||||
<PlatformDivider />
|
||||
<ContainerIcon
|
||||
style={{
|
||||
color: ANTD_GRAY[9],
|
||||
}}
|
||||
/>
|
||||
<ContainerText>
|
||||
{entityRegistry.getDisplayName(EntityType.Container, container)}
|
||||
</ContainerText>
|
||||
</Link>
|
||||
)}
|
||||
{entityCount && entityCount > 0 ? (
|
||||
<>
|
||||
<PlatformDivider />
|
||||
<EntityCountText>{entityCount.toLocaleString()} entities</EntityCountText>
|
||||
</>
|
||||
) : null}
|
||||
</PlatformContent>
|
||||
<PlatformContent />
|
||||
<div style={{ display: 'flex', justifyContent: 'left', alignItems: 'center' }}>
|
||||
<EntityTitle level={3}>{entityData?.name || ' '}</EntityTitle>
|
||||
{entityData?.deprecation?.deprecated && (
|
||||
@ -324,6 +223,7 @@ export const EntityHeader = ({ showDeprecateOption }: Props) => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<EntityCount entityCount={entityCount} />
|
||||
</MainHeaderContent>
|
||||
<SideHeaderContent>
|
||||
<TopButtonsWrapper>
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Typography } from 'antd';
|
||||
import { FolderOpenOutlined } from '@ant-design/icons';
|
||||
import { Maybe } from 'graphql/jsutils/Maybe';
|
||||
import { Container, EntityType } from '../../../../../../../types.generated';
|
||||
import { ANTD_GRAY } from '../../../../constants';
|
||||
import { useEntityRegistry } from '../../../../../../useEntityRegistry';
|
||||
|
||||
const ContainerText = styled(Typography.Text)`
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: ${ANTD_GRAY[7]};
|
||||
`;
|
||||
|
||||
const ContainerIcon = styled(FolderOpenOutlined)`
|
||||
color: ${ANTD_GRAY[7]};
|
||||
|
||||
&&& {
|
||||
font-size: 12px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
container: Maybe<Container>;
|
||||
}
|
||||
|
||||
function ContainerLink(props: Props) {
|
||||
const { container } = props;
|
||||
const entityRegistry = useEntityRegistry();
|
||||
|
||||
if (!container) return null;
|
||||
|
||||
const containerUrl = entityRegistry.getEntityUrl(EntityType.Container, container.urn);
|
||||
const containerName = entityRegistry.getDisplayName(EntityType.Container, container);
|
||||
|
||||
return (
|
||||
<StyledLink to={containerUrl} data-testid="container">
|
||||
<ContainerIcon />
|
||||
<ContainerText>{containerName}</ContainerText>
|
||||
</StyledLink>
|
||||
);
|
||||
}
|
||||
|
||||
export default ContainerLink;
|
||||
@ -0,0 +1,59 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useEntityRegistry } from '../../../../../../useEntityRegistry';
|
||||
import { IconStyleType } from '../../../../../Entity';
|
||||
import { useEntityData } from '../../../../EntityContext';
|
||||
import { capitalizeFirstLetterOnly } from '../../../../../../shared/textUtil';
|
||||
import { getPlatformName } from '../../../../utils';
|
||||
import PlatformContentView from './PlatformContentView';
|
||||
|
||||
export function useParentContainersTruncation(dataDependency: any) {
|
||||
const parentContainersRef = useRef<HTMLDivElement>(null);
|
||||
const [areContainersTruncated, setAreContainersTruncated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
parentContainersRef &&
|
||||
parentContainersRef.current &&
|
||||
parentContainersRef.current.scrollWidth > parentContainersRef.current.clientWidth
|
||||
) {
|
||||
setAreContainersTruncated(true);
|
||||
}
|
||||
}, [dataDependency]);
|
||||
|
||||
return { parentContainersRef, areContainersTruncated };
|
||||
}
|
||||
|
||||
function PlatformContentContainer() {
|
||||
const { entityType, entityData } = useEntityData();
|
||||
const entityRegistry = useEntityRegistry();
|
||||
|
||||
const basePlatformName = getPlatformName(entityData);
|
||||
const platformName = capitalizeFirstLetterOnly(basePlatformName);
|
||||
|
||||
const platformLogoUrl = entityData?.platform?.properties?.logoUrl;
|
||||
const entityLogoComponent = entityRegistry.getIcon(entityType, 12, IconStyleType.ACCENT);
|
||||
const typeIcon = entityRegistry.getIcon(entityType, 12, IconStyleType.ACCENT);
|
||||
const entityTypeCased =
|
||||
(entityData?.subTypes?.typeNames?.length && capitalizeFirstLetterOnly(entityData?.subTypes.typeNames[0])) ||
|
||||
entityRegistry.getEntityName(entityType);
|
||||
const displayedEntityType = entityData?.entityTypeOverride || entityTypeCased;
|
||||
const instanceId = entityData?.dataPlatformInstance?.instanceId;
|
||||
|
||||
const { parentContainersRef, areContainersTruncated } = useParentContainersTruncation(entityData);
|
||||
|
||||
return (
|
||||
<PlatformContentView
|
||||
platformName={platformName}
|
||||
platformLogoUrl={platformLogoUrl}
|
||||
entityLogoComponent={entityLogoComponent}
|
||||
instanceId={instanceId}
|
||||
typeIcon={typeIcon}
|
||||
entityType={displayedEntityType}
|
||||
parentContainers={entityData?.parentContainers?.containers}
|
||||
parentContainersRef={parentContainersRef}
|
||||
areContainersTruncated={areContainersTruncated}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlatformContentContainer;
|
||||
@ -0,0 +1,159 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Typography, Image, Tooltip } from 'antd';
|
||||
import { RightOutlined } from '@ant-design/icons';
|
||||
import { Maybe } from 'graphql/jsutils/Maybe';
|
||||
import { Container } from '../../../../../../../types.generated';
|
||||
import { ANTD_GRAY } from '../../../../constants';
|
||||
import ContainerLink from './ContainerLink';
|
||||
|
||||
const LogoContainer = styled.span`
|
||||
margin-right: 8px;
|
||||
`;
|
||||
|
||||
const PreviewImage = styled(Image)`
|
||||
max-height: 17px;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
background-color: transparent;
|
||||
`;
|
||||
|
||||
const PlatformContentWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 8px 8px 0;
|
||||
flex-wrap: nowrap;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const PlatformText = styled(Typography.Text)`
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
font-weight: 700;
|
||||
color: ${ANTD_GRAY[7]};
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const PlatformDivider = styled.div`
|
||||
display: inline-block;
|
||||
padding-left: 8px;
|
||||
margin-right: 8px;
|
||||
border-right: 1px solid ${ANTD_GRAY[4]};
|
||||
height: 18px;
|
||||
vertical-align: text-top;
|
||||
`;
|
||||
|
||||
const TypeIcon = styled.span`
|
||||
margin-right: 8px;
|
||||
`;
|
||||
|
||||
const StyledRightOutlined = styled(RightOutlined)`
|
||||
color: ${ANTD_GRAY[7]};
|
||||
font-size: 8px;
|
||||
margin: 0 10px;
|
||||
`;
|
||||
|
||||
const ParentContainersWrapper = styled.div`
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-direction: row-reverse;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const Ellipsis = styled.span`
|
||||
color: ${ANTD_GRAY[7]};
|
||||
margin-right: 2px;
|
||||
`;
|
||||
|
||||
const StyledTooltip = styled(Tooltip)`
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export function getParentContainerNames(containers?: Maybe<Container>[] | null) {
|
||||
let parentNames = '';
|
||||
if (containers) {
|
||||
[...containers].reverse().forEach((container, index) => {
|
||||
if (container?.properties) {
|
||||
if (index !== 0) {
|
||||
parentNames += ' > ';
|
||||
}
|
||||
parentNames += container.properties.name;
|
||||
}
|
||||
});
|
||||
}
|
||||
return parentNames;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
platformName?: string;
|
||||
platformLogoUrl?: Maybe<string>;
|
||||
entityLogoComponent?: JSX.Element;
|
||||
instanceId?: string;
|
||||
typeIcon?: JSX.Element;
|
||||
entityType?: string;
|
||||
parentContainers?: Maybe<Container>[] | null;
|
||||
parentContainersRef: React.RefObject<HTMLDivElement>;
|
||||
areContainersTruncated: boolean;
|
||||
}
|
||||
|
||||
function PlatformContentView(props: Props) {
|
||||
const {
|
||||
platformName,
|
||||
platformLogoUrl,
|
||||
entityLogoComponent,
|
||||
instanceId,
|
||||
typeIcon,
|
||||
entityType,
|
||||
parentContainers,
|
||||
parentContainersRef,
|
||||
areContainersTruncated,
|
||||
} = props;
|
||||
|
||||
const directParentContainer = parentContainers && parentContainers[0];
|
||||
const remainingParentContainers = parentContainers && parentContainers.slice(1, parentContainers.length);
|
||||
|
||||
return (
|
||||
<PlatformContentWrapper>
|
||||
{typeIcon && <TypeIcon>{typeIcon}</TypeIcon>}
|
||||
<PlatformText>{entityType}</PlatformText>
|
||||
<PlatformDivider />
|
||||
{platformName && (
|
||||
<LogoContainer>
|
||||
{(!!platformLogoUrl && <PreviewImage preview={false} src={platformLogoUrl} alt={platformName} />) ||
|
||||
entityLogoComponent}
|
||||
</LogoContainer>
|
||||
)}
|
||||
<PlatformText>
|
||||
{platformName}
|
||||
{(directParentContainer || instanceId) && <StyledRightOutlined data-testid="right-arrow" />}
|
||||
</PlatformText>
|
||||
{instanceId && (
|
||||
<PlatformText>
|
||||
{instanceId}
|
||||
{directParentContainer && <StyledRightOutlined data-testid="right-arrow" />}
|
||||
</PlatformText>
|
||||
)}
|
||||
<StyledTooltip
|
||||
title={getParentContainerNames(parentContainers)}
|
||||
overlayStyle={areContainersTruncated ? {} : { display: 'none' }}
|
||||
>
|
||||
{areContainersTruncated && <Ellipsis>...</Ellipsis>}
|
||||
<ParentContainersWrapper ref={parentContainersRef}>
|
||||
{remainingParentContainers &&
|
||||
remainingParentContainers.map((container) => (
|
||||
<span key={container?.urn}>
|
||||
<ContainerLink container={container} />
|
||||
<StyledRightOutlined data-testid="right-arrow" />
|
||||
</span>
|
||||
))}
|
||||
</ParentContainersWrapper>
|
||||
{directParentContainer && <ContainerLink container={directParentContainer} />}
|
||||
</StyledTooltip>
|
||||
</PlatformContentWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlatformContentView;
|
||||
@ -0,0 +1,3 @@
|
||||
import PlatformContentContainer from './PlatformContentContainer';
|
||||
|
||||
export default PlatformContentContainer;
|
||||
@ -26,6 +26,7 @@ import {
|
||||
Status,
|
||||
Deprecation,
|
||||
DataPlatformInstance,
|
||||
ParentContainersResult,
|
||||
} from '../../../types.generated';
|
||||
import { FetchedEntity } from '../../lineage/types';
|
||||
|
||||
@ -78,6 +79,7 @@ export type GenericEntityProperties = {
|
||||
health?: Maybe<Health>;
|
||||
status?: Maybe<Status>;
|
||||
deprecation?: Maybe<Deprecation>;
|
||||
parentContainers?: Maybe<ParentContainersResult>;
|
||||
};
|
||||
|
||||
export type GenericEntityUpdate = {
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { GenericEntityProperties } from './types';
|
||||
|
||||
export function urlEncodeUrn(urn: string) {
|
||||
return (
|
||||
urn &&
|
||||
@ -58,4 +60,8 @@ export const singularizeCollectionName = (collectionName: string): string => {
|
||||
return collectionName;
|
||||
};
|
||||
|
||||
export function getPlatformName(entityData: GenericEntityProperties | null) {
|
||||
return entityData?.platform?.properties?.displayName || entityData?.platform?.name;
|
||||
}
|
||||
|
||||
export const EDITED_DESCRIPTIONS_CACHE_NAME = 'editedDescriptions';
|
||||
|
||||
@ -1,10 +1,17 @@
|
||||
import { Image, Tooltip, Typography } from 'antd';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { FolderOpenOutlined } from '@ant-design/icons';
|
||||
import { Tooltip, Typography } from 'antd';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { GlobalTags, Owner, GlossaryTerms, SearchInsight, Container, EntityType, Domain } from '../../types.generated';
|
||||
import {
|
||||
GlobalTags,
|
||||
Owner,
|
||||
GlossaryTerms,
|
||||
SearchInsight,
|
||||
Container,
|
||||
Domain,
|
||||
ParentContainersResult,
|
||||
} from '../../types.generated';
|
||||
import { useEntityRegistry } from '../useEntityRegistry';
|
||||
|
||||
import AvatarsGroup from '../shared/avatar/AvatarsGroup';
|
||||
@ -13,10 +20,9 @@ import { ANTD_GRAY } from '../entity/shared/constants';
|
||||
import NoMarkdownViewer from '../entity/shared/components/styled/StripMarkdownText';
|
||||
import { getNumberWithOrdinal } from '../entity/shared/utils';
|
||||
import { useEntityData } from '../entity/shared/EntityContext';
|
||||
|
||||
const LogoContainer = styled.div`
|
||||
padding-right: 8px;
|
||||
`;
|
||||
import PlatformContentView from '../entity/shared/containers/profile/header/PlatformContent/PlatformContentView';
|
||||
import { useParentContainersTruncation } from '../entity/shared/containers/profile/header/PlatformContent/PlatformContentContainer';
|
||||
import EntityCount from '../entity/shared/containers/profile/header/EntityCount';
|
||||
|
||||
const PreviewContainer = styled.div`
|
||||
display: flex;
|
||||
@ -29,27 +35,18 @@ const PreviewWrapper = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const PlatformInfo = styled.div`
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
`;
|
||||
|
||||
const TitleContainer = styled.div`
|
||||
margin-bottom: 5px;
|
||||
line-height: 30px;
|
||||
`;
|
||||
|
||||
const PreviewImage = styled(Image)`
|
||||
max-height: 18px;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
margin-right: 8px;
|
||||
background-color: transparent;
|
||||
.entityCount {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
`;
|
||||
|
||||
const EntityTitle = styled(Typography.Text)<{ $titleSizePx?: number }>`
|
||||
display: block;
|
||||
|
||||
&&& {
|
||||
margin-right 8px;
|
||||
font-size: ${(props) => props.$titleSizePx || 16}px;
|
||||
@ -65,13 +62,6 @@ const PlatformText = styled(Typography.Text)`
|
||||
color: ${ANTD_GRAY[7]};
|
||||
`;
|
||||
|
||||
const EntityCountText = styled(Typography.Text)`
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
font-weight: 400;
|
||||
color: ${ANTD_GRAY[7]};
|
||||
`;
|
||||
|
||||
const PlatformDivider = styled.div`
|
||||
display: inline-block;
|
||||
padding-left: 10px;
|
||||
@ -117,24 +107,6 @@ const InsightIconContainer = styled.span`
|
||||
margin-right: 4px;
|
||||
`;
|
||||
|
||||
const TypeIcon = styled.span`
|
||||
margin-right: 8px;
|
||||
`;
|
||||
|
||||
const ContainerText = styled(Typography.Text)`
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
font-weight: 400;
|
||||
color: ${ANTD_GRAY[9]};
|
||||
`;
|
||||
|
||||
const ContainerIcon = styled(FolderOpenOutlined)`
|
||||
&&& {
|
||||
font-size: 12px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
logoUrl?: string;
|
||||
@ -160,6 +132,7 @@ interface Props {
|
||||
// this is provided by the impact analysis view. it is used to display
|
||||
// how the listed node is connected to the source node
|
||||
degree?: number;
|
||||
parentContainers?: ParentContainersResult | null;
|
||||
}
|
||||
|
||||
export default function DefaultPreviewCard({
|
||||
@ -187,6 +160,7 @@ export default function DefaultPreviewCard({
|
||||
dataTestID,
|
||||
onClick,
|
||||
degree,
|
||||
parentContainers,
|
||||
}: 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
|
||||
@ -206,59 +180,38 @@ export default function DefaultPreviewCard({
|
||||
insightViews.push(snippet);
|
||||
}
|
||||
|
||||
const { parentContainersRef, areContainersTruncated } = useParentContainersTruncation(container);
|
||||
|
||||
return (
|
||||
<PreviewContainer data-testid={dataTestID}>
|
||||
<PreviewWrapper>
|
||||
<TitleContainer>
|
||||
<Link to={url}>
|
||||
<PlatformInfo>
|
||||
{(logoUrl && <PreviewImage preview={false} src={logoUrl} alt={platform || ''} />) || (
|
||||
<LogoContainer>{logoComponent}</LogoContainer>
|
||||
)}
|
||||
{platform && (
|
||||
<PlatformText>
|
||||
{platform}
|
||||
{platformInstanceId && ` - ${platformInstanceId}`}
|
||||
</PlatformText>
|
||||
)}
|
||||
{(logoUrl || logoComponent || platform) && <PlatformDivider />}
|
||||
{typeIcon && <TypeIcon>{typeIcon}</TypeIcon>}
|
||||
<PlatformText>{type}</PlatformText>
|
||||
{container && (
|
||||
<Link to={entityRegistry.getEntityUrl(EntityType.Container, container?.urn)}>
|
||||
<PlatformDivider />
|
||||
<ContainerIcon
|
||||
style={{
|
||||
color: ANTD_GRAY[9],
|
||||
}}
|
||||
/>
|
||||
<ContainerText>
|
||||
{entityRegistry.getDisplayName(EntityType.Container, container)}
|
||||
</ContainerText>
|
||||
</Link>
|
||||
)}
|
||||
{entityCount && entityCount > 0 ? (
|
||||
<>
|
||||
<PlatformDivider />
|
||||
<EntityCountText>{entityCount.toLocaleString()} entities</EntityCountText>
|
||||
</>
|
||||
) : null}
|
||||
{degree !== undefined && degree !== null && (
|
||||
<span>
|
||||
<PlatformDivider />
|
||||
<Tooltip
|
||||
title={`This entity is a ${getNumberWithOrdinal(degree)} degree connection to ${
|
||||
entityData?.name || 'the source entity'
|
||||
}`}
|
||||
>
|
||||
<PlatformText>{getNumberWithOrdinal(degree)}</PlatformText>
|
||||
</Tooltip>
|
||||
</span>
|
||||
)}
|
||||
</PlatformInfo>
|
||||
<PlatformContentView
|
||||
platformName={platform}
|
||||
platformLogoUrl={logoUrl}
|
||||
entityLogoComponent={logoComponent}
|
||||
instanceId={platformInstanceId}
|
||||
typeIcon={typeIcon}
|
||||
entityType={type}
|
||||
parentContainers={parentContainers?.containers}
|
||||
parentContainersRef={parentContainersRef}
|
||||
areContainersTruncated={areContainersTruncated}
|
||||
/>
|
||||
<EntityTitle onClick={onClick} $titleSizePx={titleSizePx}>
|
||||
{name || ' '}
|
||||
</EntityTitle>
|
||||
{degree !== undefined && degree !== null && (
|
||||
<Tooltip
|
||||
title={`This entity is a ${getNumberWithOrdinal(degree)} degree connection to ${
|
||||
entityData?.name || 'the source entity'
|
||||
}`}
|
||||
>
|
||||
<PlatformText>{getNumberWithOrdinal(degree)}</PlatformText>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!!degree && entityCount && <PlatformDivider />}
|
||||
<EntityCount entityCount={entityCount} />
|
||||
</Link>
|
||||
</TitleContainer>
|
||||
{description && description.length > 0 && (
|
||||
|
||||
@ -14,6 +14,7 @@ import { CustomAvatar } from '../shared/avatar';
|
||||
import { StyledTag } from '../entity/shared/components/styled/StyledTag';
|
||||
import { useListRecommendationsQuery } from '../../graphql/recommendations.generated';
|
||||
import { useGetAuthenticatedUserUrn } from '../useGetAuthenticatedUser';
|
||||
import { getPlatformName } from '../entity/shared/utils';
|
||||
|
||||
const SuggestionContainer = styled.div`
|
||||
display: flex;
|
||||
@ -109,7 +110,7 @@ const renderEntitySuggestion = (query: string, entity: Entity, registry: EntityR
|
||||
return renderTagSuggestion(entity as Tag, registry);
|
||||
}
|
||||
const genericEntityProps = registry.getGenericEntityProperties(entity.type, entity);
|
||||
const platformName = genericEntityProps?.platform?.properties?.displayName || genericEntityProps?.platform?.name;
|
||||
const platformName = getPlatformName(genericEntityProps);
|
||||
const platformLogoUrl = genericEntityProps?.platform?.properties?.logoUrl;
|
||||
const displayName =
|
||||
genericEntityProps?.properties?.qualifiedName ||
|
||||
|
||||
@ -59,6 +59,9 @@ query getChart($urn: String!) {
|
||||
container {
|
||||
...entityContainer
|
||||
}
|
||||
parentContainers {
|
||||
...parentContainersFields
|
||||
}
|
||||
upstream: lineage(input: { direction: UPSTREAM, start: 0, count: 100 }) {
|
||||
...fullLineageResults
|
||||
}
|
||||
|
||||
@ -36,6 +36,9 @@ query getContainer($urn: String!) {
|
||||
container {
|
||||
...entityContainer
|
||||
}
|
||||
parentContainers {
|
||||
...parentContainersFields
|
||||
}
|
||||
domain {
|
||||
...entityDomain
|
||||
}
|
||||
|
||||
@ -64,6 +64,9 @@ query getDataset($urn: String!) {
|
||||
container {
|
||||
...entityContainer
|
||||
}
|
||||
parentContainers {
|
||||
...parentContainersFields
|
||||
}
|
||||
usageStats(range: MONTH) {
|
||||
buckets {
|
||||
bucket
|
||||
|
||||
@ -30,6 +30,14 @@ fragment deprecationFields on Deprecation {
|
||||
decommissionTime
|
||||
}
|
||||
|
||||
fragment parentContainersFields on ParentContainersResult {
|
||||
count
|
||||
containers {
|
||||
...entityContainer
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fragment ownershipFields on Ownership {
|
||||
owners {
|
||||
owner {
|
||||
@ -352,6 +360,9 @@ fragment dashboardFields on Dashboard {
|
||||
container {
|
||||
...entityContainer
|
||||
}
|
||||
parentContainers {
|
||||
...parentContainersFields
|
||||
}
|
||||
status {
|
||||
removed
|
||||
}
|
||||
|
||||
@ -65,6 +65,9 @@ fragment searchResultFields on Entity {
|
||||
container {
|
||||
...entityContainer
|
||||
}
|
||||
parentContainers {
|
||||
...parentContainersFields
|
||||
}
|
||||
deprecation {
|
||||
...deprecationFields
|
||||
}
|
||||
@ -146,6 +149,9 @@ fragment searchResultFields on Entity {
|
||||
deprecation {
|
||||
...deprecationFields
|
||||
}
|
||||
parentContainers {
|
||||
...parentContainersFields
|
||||
}
|
||||
}
|
||||
... on Chart {
|
||||
urn
|
||||
@ -189,6 +195,9 @@ fragment searchResultFields on Entity {
|
||||
deprecation {
|
||||
...deprecationFields
|
||||
}
|
||||
parentContainers {
|
||||
...parentContainersFields
|
||||
}
|
||||
}
|
||||
... on DataFlow {
|
||||
urn
|
||||
@ -327,6 +336,9 @@ fragment searchResultFields on Entity {
|
||||
deprecation {
|
||||
...deprecationFields
|
||||
}
|
||||
parentContainers {
|
||||
...parentContainersFields
|
||||
}
|
||||
}
|
||||
... on MLFeatureTable {
|
||||
urn
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user