feat(react): show primary keys & foreign keys in the schema (#3298)

This commit is contained in:
Gabe Lyons 2021-09-28 22:47:00 -07:00 committed by GitHub
parent febbced383
commit 08e18868dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 807 additions and 72 deletions

View File

@ -17,6 +17,7 @@ import com.linkedin.datahub.graphql.generated.Dataset;
import com.linkedin.datahub.graphql.generated.Entity;
import com.linkedin.datahub.graphql.generated.EntityRelationship;
import com.linkedin.datahub.graphql.generated.EntityRelationshipLegacy;
import com.linkedin.datahub.graphql.generated.ForeignKeyConstraint;
import com.linkedin.datahub.graphql.generated.MLModelProperties;
import com.linkedin.datahub.graphql.generated.RelatedDataset;
import com.linkedin.datahub.graphql.generated.SearchResult;
@ -511,6 +512,12 @@ public class GmsGraphQLEngine {
(env) -> ((RelatedDataset) env.getSource()).getDataset().getUrn()))
)
)
.type("ForeignKeyConstraint", typeWiring -> typeWiring
.dataFetcher("foreignDataset", new AuthenticatedResolver<>(
new LoadableTypeResolver<>(datasetType,
(env) -> ((ForeignKeyConstraint) env.getSource()).getForeignDataset().getUrn()))
)
)
.type("InstitutionalMemoryMetadata", typeWiring -> typeWiring
.dataFetcher("author", new AuthenticatedResolver<>(
new LoadableTypeResolver<>(corpUserType,

View File

@ -0,0 +1,41 @@
package com.linkedin.datahub.graphql.types.dataset.mappers;
import com.linkedin.common.urn.Urn;
import com.linkedin.datahub.graphql.generated.Dataset;
import com.linkedin.datahub.graphql.generated.ForeignKeyConstraint;
import com.linkedin.datahub.graphql.generated.SchemaFieldEntity;
import com.linkedin.datahub.graphql.types.common.mappers.UrnToEntityMapper;
import java.util.stream.Collectors;
public class ForeignKeyConstraintMapper {
private ForeignKeyConstraintMapper() { }
public static ForeignKeyConstraint map(com.linkedin.schema.ForeignKeyConstraint constraint) {
ForeignKeyConstraint result = new ForeignKeyConstraint();
result.setName(constraint.getName());
if (constraint.hasForeignDataset()) {
result.setForeignDataset((Dataset) UrnToEntityMapper.map(constraint.getForeignDataset()));
}
if (constraint.hasSourceFields()) {
result.setSourceFields(
constraint.getSourceFields().stream().map(
schemaFieldUrn -> mapSchemaFieldEntity(schemaFieldUrn)
).collect(Collectors.toList()));
}
if (constraint.hasForeignFields()) {
result.setForeignFields(
constraint.getForeignFields().stream().map(
schemaFieldUrn -> mapSchemaFieldEntity(schemaFieldUrn)
).collect(Collectors.toList()));
}
return result;
}
private static SchemaFieldEntity mapSchemaFieldEntity(Urn schemaFieldUrn) {
SchemaFieldEntity result = new SchemaFieldEntity();
result.setParent(schemaFieldUrn.getEntityKey().get(0));
result.setFieldPath(schemaFieldUrn.getEntityKey().get(1));
return result;
}
}

View File

@ -33,6 +33,11 @@ public class SchemaMetadataMapper implements ModelMapper<VersionedAspect, com.li
result.setFields(input.getFields().stream().map(SchemaFieldMapper::map).collect(Collectors.toList()));
result.setPlatformSchema(PlatformSchemaMapper.map(input.getPlatformSchema()));
result.setAspectVersion(inputWithMetadata.getVersion());
if (input.hasForeignKeys()) {
result.setForeignKeys(input.getForeignKeys().stream().map(foreignKeyConstraint -> ForeignKeyConstraintMapper.map(
foreignKeyConstraint
)).collect(Collectors.toList()));
}
return result;
}
}

View File

@ -1035,12 +1035,42 @@ type SchemaMetadata implements Aspect {
"""
primaryKeys: [String!]
"""
Client provided list of foreign key constraints
"""
foreignKeys: [ForeignKeyConstraint]
"""
The time at which the schema metadata information was created
"""
createdAt: Long
}
"""
Metadata around a foreign key constraint between two datasets
"""
type ForeignKeyConstraint {
"""
The human-readable name of the constraint
"""
name: String
"""
List of fields in the foreign dataset
"""
foreignFields: [SchemaFieldEntity]
"""
List of fields in this dataset
"""
sourceFields: [SchemaFieldEntity]
"""
The foreign dataset for easy reference
"""
foreignDataset: Dataset
}
"""
Deprecated, use SchemaMetadata instead
Metadata about a Dataset schema
@ -1122,6 +1152,27 @@ type KeyValueSchema {
valueSchema: String!
}
"""
Standalone schema field entity. Differs from the SchemaField struct because it is not directly nested inside a
schema field
"""
type SchemaFieldEntity {
"""
Primary key of the schema field
"""
urn: String!
"""
Field path identifying the field in its dataset
"""
fieldPath: String!
"""
The primary key of the field's parent.
"""
parent: String!
}
"""
Information about an individual field in a Dataset schema
"""
@ -4806,4 +4857,3 @@ enum CostType {
"""
ORG_COST_TYPE
}

View File

@ -377,6 +377,7 @@ export const dataset3 = {
},
datasetUrn: 'urn:li:dataset:3',
primaryKeys: [],
foreignKeys: [],
},
previousSchemaMetadata: null,
editableSchemaMetadata: null,

View File

@ -3,7 +3,7 @@ import { fireEvent, render } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import TestPageContainer from '../../../../../utils/test-utils/TestPageContainer';
import { sampleSchema, sampleSchemaWithTags } from '../stories/sampleSchema';
import { sampleSchema, sampleSchemaWithPkFk, sampleSchemaWithTags } from '../stories/sampleSchema';
import { mocks } from '../../../../../Mocks';
import { SchemaTab } from '../../../shared/tabs/Dataset/Schema/SchemaTab';
import EntityContext from '../../../shared/EntityContext';
@ -154,4 +154,62 @@ describe('Schema', () => {
);
expect(getByText('shipping_address')).toBeInTheDocument();
});
it('renders primary keys', () => {
const { getByText } = render(
<MockedProvider mocks={mocks} addTypename={false}>
<TestPageContainer>
<EntityContext.Provider
value={{
urn: 'urn:li:dataset:123',
entityType: EntityType.Dataset,
entityData: {
description: 'This is a description',
schemaMetadata: sampleSchemaWithPkFk as SchemaMetadata,
},
baseEntity: {},
updateEntity: jest.fn(),
routeToTab: jest.fn(),
refetch: jest.fn(),
}}
>
<SchemaTab />
</EntityContext.Provider>
</TestPageContainer>
</MockedProvider>,
);
expect(getByText('Primary Key')).toBeInTheDocument();
});
it('renders foreign keys', () => {
const { getByText, getAllByText } = render(
<MockedProvider mocks={mocks} addTypename={false}>
<TestPageContainer>
<EntityContext.Provider
value={{
urn: 'urn:li:dataset:123',
entityType: EntityType.Dataset,
entityData: {
description: 'This is a description',
schemaMetadata: sampleSchemaWithPkFk as SchemaMetadata,
},
baseEntity: {},
updateEntity: jest.fn(),
routeToTab: jest.fn(),
refetch: jest.fn(),
}}
>
<SchemaTab />
</EntityContext.Provider>
</TestPageContainer>
</MockedProvider>,
);
expect(getByText('Foreign Key')).toBeInTheDocument();
const fkButton = getByText('Foreign Key');
fireEvent.click(fkButton);
expect(getByText('Foreign Key to')).toBeInTheDocument();
expect(getAllByText('Yet Another Dataset')).toHaveLength(2);
});
});

View File

@ -1,9 +1,12 @@
import React from 'react';
import React, { useState } from 'react';
import { Typography } from 'antd';
import styled from 'styled-components';
import translateFieldPath from './translateFieldPath';
import { ExtendedSchemaFields } from './types';
import TypeLabel from '../../../../shared/tabs/Dataset/Schema/components/TypeLabel';
import { ForeignKeyConstraint, SchemaMetadata } from '../../../../../../types.generated';
import PrimaryKeyLabel from '../../../../shared/tabs/Dataset/Schema/components/PrimaryKeyLabel';
import ForeignKeyLabel from '../../../../shared/tabs/Dataset/Schema/components/ForeignKeyLabel';
const MAX_FIELD_PATH_LENGTH = 200;
@ -12,6 +15,7 @@ const MAX_FIELD_PATH_LENGTH = 200;
// `;
const FieldPathContainer = styled.div`
vertical-align: top;
display: inline-block;
width: 250px;
margin-top: 16px;
@ -25,31 +29,56 @@ const FieldPathText = styled(Typography.Text)`
`;
// ex: [type=MetadataAuditEvent].[type=union]oldSnapshot.[type=CorpUserSnapshot].[type=array]aspects.[type=union].[type=CorpUserInfo].[type=boolean]active
export default function schemaTitleRenderer(fieldPath: string, record: ExtendedSchemaFields) {
const fieldPathWithoutAnnotations = translateFieldPath(fieldPath);
export default function useSchemaTitleRenderer(
schemaMetadata: SchemaMetadata | undefined | null,
setSelectedFkFieldPath: (params: { fieldPath: string; constraint?: ForeignKeyConstraint | null } | null) => void,
) {
const [highlightedConstraint, setHighlightedConstraint] = useState<string | null>(null);
const isOverflow = fieldPathWithoutAnnotations.length > MAX_FIELD_PATH_LENGTH;
return (fieldPath: string, record: ExtendedSchemaFields): JSX.Element => {
const fieldPathWithoutAnnotations = translateFieldPath(fieldPath);
let [firstPath, lastPath] = fieldPathWithoutAnnotations.split(/\.(?=[^.]+$)/);
const isOverflow = fieldPathWithoutAnnotations.length > MAX_FIELD_PATH_LENGTH;
if (isOverflow) {
if (lastPath.length >= MAX_FIELD_PATH_LENGTH) {
lastPath = `..${lastPath.substring(lastPath.length - MAX_FIELD_PATH_LENGTH)}`;
firstPath = '';
} else {
firstPath = firstPath.substring(fieldPath.length - MAX_FIELD_PATH_LENGTH);
if (firstPath.includes('.')) {
firstPath = `..${firstPath.substring(firstPath.indexOf('.'))}`;
let [firstPath, lastPath] = fieldPathWithoutAnnotations.split(/\.(?=[^.]+$)/);
if (isOverflow) {
if (lastPath.length >= MAX_FIELD_PATH_LENGTH) {
lastPath = `..${lastPath.substring(lastPath.length - MAX_FIELD_PATH_LENGTH)}`;
firstPath = '';
} else {
firstPath = '..';
firstPath = firstPath.substring(fieldPath.length - MAX_FIELD_PATH_LENGTH);
if (firstPath.includes('.')) {
firstPath = `..${firstPath.substring(firstPath.indexOf('.'))}`;
} else {
firstPath = '..';
}
}
}
}
return (
<FieldPathContainer>
<FieldPathText>{lastPath || firstPath}</FieldPathText>
<TypeLabel type={record.type} nativeDataType={record.nativeDataType} />
</FieldPathContainer>
);
return (
<>
<FieldPathContainer>
<FieldPathText>{lastPath || firstPath}</FieldPathText>
<TypeLabel type={record.type} nativeDataType={record.nativeDataType} />
{schemaMetadata?.primaryKeys?.includes(fieldPath) && <PrimaryKeyLabel />}
{schemaMetadata?.foreignKeys
?.filter(
(constraint) =>
(constraint?.sourceFields?.filter((sourceField) => sourceField?.fieldPath === fieldPath)
.length || 0) > 0,
)
.map((constraint) => (
<ForeignKeyLabel
fieldPath={fieldPath}
constraint={constraint}
highlight={constraint?.name === highlightedConstraint}
setHighlightedConstraint={setHighlightedConstraint}
onClick={setSelectedFkFieldPath}
/>
))}
</FieldPathContainer>
</>
);
};
}

View File

@ -1,3 +1,4 @@
import { dataset3 } from '../../../../../Mocks';
import { EntityType, Schema, SchemaMetadata, SchemaField, SchemaFieldDataType } from '../../../../../types.generated';
// Extending the schema type with an option for tags
@ -190,3 +191,125 @@ export const sampleSchemaWithTags: Schema = {
} as SchemaField,
],
};
export const sampleSchemaWithPkFk: SchemaMetadata = {
primaryKeys: ['name'],
foreignKeys: [
{
name: 'constraint',
sourceFields: [
{
urn: 'datasetUrn',
parent: 'dataset',
fieldPath: 'shipping_address',
},
],
foreignFields: [
{
urn: dataset3.urn,
parent: dataset3.name,
fieldPath: 'address',
},
],
foreignDataset: dataset3,
},
],
name: 'MockSchema',
platformUrn: 'mock:urn',
version: 1,
hash: '',
fields: [
{
fieldPath: 'id',
nullable: false,
description: 'order id',
type: SchemaFieldDataType.Number,
nativeDataType: 'number',
recursive: false,
globalTags: {
tags: [
{
tag: {
urn: 'urn:li:tag:Legacy',
name: 'Legacy',
description: 'this is a legacy dataset',
type: EntityType.Tag,
},
},
],
},
glossaryTerms: {
terms: [
{
term: {
type: EntityType.GlossaryTerm,
urn: 'urn:li:glossaryTerm:sample-glossary-term',
name: 'sample-glossary-term',
hierarchicalName: 'example.sample-glossary-term',
glossaryTermInfo: {
definition: 'sample definition',
termSource: 'sample term source',
},
},
},
],
},
},
{
fieldPath: 'name',
nullable: true,
description: 'the name of the order',
type: SchemaFieldDataType.String,
nativeDataType: 'string',
recursive: false,
} as SchemaField,
{
fieldPath: 'shipping_address',
nullable: true,
description: 'the address the order ships to',
type: SchemaFieldDataType.String,
nativeDataType: 'string',
recursive: false,
} as SchemaField,
{
fieldPath: 'count',
nullable: true,
description: 'the number of items in the order',
type: SchemaFieldDataType.Number,
nativeDataType: 'number',
recursive: false,
},
{
fieldPath: 'cost',
nullable: true,
description: 'the dollar value of the order',
type: SchemaFieldDataType.Number,
nativeDataType: 'number',
recursive: false,
} as SchemaField,
{
fieldPath: 'was_returned',
nullable: true,
description: 'if the order was sent back',
type: SchemaFieldDataType.Boolean,
nativeDataType: 'boolean',
recursive: false,
},
{
fieldPath: 'payload',
nullable: true,
description: 'payload attached to the order',
type: SchemaFieldDataType.Bytes,
nativeDataType: 'bytes',
recursive: false,
},
{
fieldPath: 'payment_information',
nullable: true,
description: 'struct representing the payment information',
type: SchemaFieldDataType.Struct,
nativeDataType: 'struct',
recursive: false,
} as SchemaField,
],
};

View File

@ -61,6 +61,7 @@ export const SchemaTab = () => {
) : rows && rows.length > 0 ? (
<>
<SchemaTable
schemaMetadata={schemaMetadata}
rows={rows}
editMode
editableSchemaMetadata={editableSchemaMetadata}

View File

@ -1,14 +1,24 @@
import React, { useMemo, useState } from 'react';
import { ColumnsType } from 'antd/es/table';
import styled from 'styled-components';
import { EditableSchemaMetadata, SchemaField, UsageQueryResult } from '../../../../../../types.generated';
import schemaTitleRenderer from '../../../../dataset/profile/schema/utils/schemaTitleRenderer';
import {} from 'antd';
import {
EditableSchemaMetadata,
ForeignKeyConstraint,
SchemaField,
SchemaMetadata,
UsageQueryResult,
} from '../../../../../../types.generated';
import useSchemaTitleRenderer from '../../../../dataset/profile/schema/utils/schemaTitleRenderer';
import { ExtendedSchemaFields } from '../../../../dataset/profile/schema/utils/types';
import useDescriptionRenderer from './utils/useDescriptionRenderer';
import useUsageStatsRenderer from './utils/useUsageStatsRenderer';
import useTagsAndTermsRenderer from './utils/useTagsAndTermsRenderer';
import ExpandIcon from './components/ExpandIcon';
import { StyledTable } from '../../../components/styled/StyledTable';
import { SchemaRow } from './components/SchemaRow';
import { FkContext } from './utils/selectedFkContext';
const TableContainer = styled.div`
&&& .ant-table-tbody > tr > .ant-table-cell-with-append {
@ -19,29 +29,31 @@ const TableContainer = styled.div`
&&& .ant-table-tbody > tr > .ant-table-cell {
border-right: none;
}
&&& .open-fk-row > td {
padding-bottom: 600px;
vertical-align: top;
}
`;
const defaultColumns = [
{
title: 'Field',
dataIndex: 'fieldPath',
key: 'fieldPath',
width: 250,
render: schemaTitleRenderer,
filtered: true,
},
];
export type Props = {
rows: Array<ExtendedSchemaFields>;
schemaMetadata: SchemaMetadata | undefined | null;
editableSchemaMetadata?: EditableSchemaMetadata | null;
editMode?: boolean;
usageStats?: UsageQueryResult | null;
};
export default function SchemaTable({ rows, editableSchemaMetadata, usageStats, editMode = true }: Props) {
export default function SchemaTable({
rows,
schemaMetadata,
editableSchemaMetadata,
usageStats,
editMode = true,
}: Props): JSX.Element {
const hasUsageStats = useMemo(() => (usageStats?.aggregations?.fields?.length || 0) > 0, [usageStats]);
const [tagHoveredIndex, setTagHoveredIndex] = useState<string | undefined>(undefined);
const [selectedFkFieldPath, setSelectedFkFieldPath] =
useState<null | { fieldPath: string; constraint?: ForeignKeyConstraint | null }>(null);
const descriptionRender = useDescriptionRenderer(editableSchemaMetadata);
const usageStatsRenderer = useUsageStatsRenderer(usageStats);
@ -53,6 +65,7 @@ export default function SchemaTable({ rows, editableSchemaMetadata, usageStats,
showTags: false,
showTerms: true,
});
const schemaTitleRenderer = useSchemaTitleRenderer(schemaMetadata, setSelectedFkFieldPath);
const onTagTermCell = (record: SchemaField, rowIndex: number | undefined) => ({
onMouseEnter: () => {
@ -67,6 +80,15 @@ export default function SchemaTable({ rows, editableSchemaMetadata, usageStats,
},
});
const fieldColumn = {
title: 'Field',
dataIndex: 'fieldPath',
key: 'fieldPath',
width: 250,
render: schemaTitleRenderer,
filtered: true,
};
const tagColumn = {
width: 125,
title: 'Tags',
@ -100,26 +122,36 @@ export default function SchemaTable({ rows, editableSchemaMetadata, usageStats,
width: 300,
};
let allColumns: ColumnsType<ExtendedSchemaFields> = [...defaultColumns, descriptionColumn, tagColumn, termColumn];
let allColumns: ColumnsType<ExtendedSchemaFields> = [fieldColumn, descriptionColumn, tagColumn, termColumn];
if (hasUsageStats) {
allColumns = [...allColumns, usageColumn];
}
return (
<TableContainer>
<StyledTable
columns={allColumns}
dataSource={rows}
rowKey="fieldPath"
expandable={{
defaultExpandAllRows: false,
expandRowByClick: false,
expandIcon: ExpandIcon,
indentSize: 0,
}}
pagination={false}
/>
</TableContainer>
<FkContext.Provider value={selectedFkFieldPath}>
<TableContainer>
<StyledTable
rowClassName={(record) =>
record.fieldPath === selectedFkFieldPath?.fieldPath ? 'open-fk-row' : ''
}
columns={allColumns}
dataSource={rows}
rowKey="fieldPath"
components={{
body: {
row: SchemaRow,
},
}}
expandable={{
defaultExpandAllRows: false,
expandRowByClick: false,
expandIcon: ExpandIcon,
indentSize: 0,
}}
pagination={false}
/>
</TableContainer>
</FkContext.Provider>
);
}

View File

@ -0,0 +1,96 @@
import React, { useContext, useState } from 'react';
import { Badge, Table } from 'antd';
import styled from 'styled-components';
import { green } from '@ant-design/colors';
import Modal from 'antd/lib/modal/Modal';
import { Link } from 'react-router-dom';
import { ANTD_GRAY } from '../../../../constants';
import { EntityType, ForeignKeyConstraint } from '../../../../../../../types.generated';
import { useBaseEntity } from '../../../../EntityContext';
import { GetDatasetQuery } from '../../../../../../../graphql/dataset.generated';
import { useEntityRegistry } from '../../../../../../useEntityRegistry';
import { FkContext } from '../utils/selectedFkContext';
const ForeignKeyBadge = styled(Badge)<{ highlight: boolean }>`
margin-left: 4px;
&&& .ant-badge-count {
background-color: ${(props) => (props.highlight ? green[1] : ANTD_GRAY[1])};
color: ${green[5]};
border: 1px solid ${green[2]};
font-size: 12px;
font-weight: 400;
height: 22px;
cursor: pointer;
}
`;
type Props = {
highlight: boolean;
fieldPath: string;
constraint?: ForeignKeyConstraint | null;
setHighlightedConstraint: (newActiveConstraint: string | null) => void;
onClick: (params: { fieldPath: string; constraint?: ForeignKeyConstraint | null } | null) => void;
};
const zip = (a, b) =>
Array.from(Array(Math.max(b.length, a.length)), (_, i) => ({ source: a[i]?.fieldPath, foreign: b[i]?.fieldPath }));
export default function ForeignKeyLabel({
fieldPath,
constraint,
highlight,
setHighlightedConstraint,
onClick,
}: Props) {
const selectedFk = useContext(FkContext);
const [showModal, setShowModal] = useState(false);
const baseEntity = useBaseEntity<GetDatasetQuery>();
const entityRegistry = useEntityRegistry();
const sourceColumn = {
title: (
<Link to={entityRegistry.getEntityUrl(EntityType.Dataset, baseEntity?.dataset?.urn || '')}>
{baseEntity.dataset?.name}
</Link>
),
dataIndex: 'source',
key: 'source',
};
const foreignColumn = {
title: (
<Link to={entityRegistry.getEntityUrl(EntityType.Dataset, constraint?.foreignDataset?.urn || '')}>
{constraint?.foreignDataset?.name}
</Link>
),
dataIndex: 'foreign',
key: 'foreign',
};
const rows = zip(constraint?.sourceFields, constraint?.foreignFields);
return (
<>
<Modal title={constraint?.name || 'Foreign Key'} visible={showModal} onCancel={() => setShowModal(false)}>
<Table columns={[sourceColumn, foreignColumn]} dataSource={rows} pagination={false} />
</Modal>
<span
role="button"
tabIndex={0}
onKeyPress={(e) => (e.key === 'Enter' ? setShowModal(true) : null)}
onClick={() => {
if (selectedFk?.fieldPath === fieldPath && selectedFk?.constraint?.name === constraint?.name) {
onClick(null);
} else {
onClick({ fieldPath, constraint });
}
}}
onMouseEnter={() => setHighlightedConstraint(constraint?.name || null)}
onMouseLeave={() => setHighlightedConstraint(null)}
>
<ForeignKeyBadge highlight={highlight || selectedFk?.fieldPath === fieldPath} count="Foreign Key" />
</span>
</>
);
}

View File

@ -0,0 +1,22 @@
import React from 'react';
import { Badge } from 'antd';
import styled from 'styled-components';
import { blue } from '@ant-design/colors';
import { ANTD_GRAY } from '../../../../constants';
const PrimaryKeyBadge = styled(Badge)`
margin-left: 4px;
&&& .ant-badge-count {
background-color: ${ANTD_GRAY[1]};
color: ${blue[5]};
border: 1px solid ${blue[2]};
font-size: 12px;
font-weight: 400;
height: 22px;
}
`;
export default function PrimaryKeyLabel() {
return <PrimaryKeyBadge count="Primary Key" />;
}

View File

@ -0,0 +1,151 @@
import { Badge } from 'antd';
import React, { useContext } from 'react';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { GetDatasetQuery } from '../../../../../../../graphql/dataset.generated';
import { EntityType } from '../../../../../../../types.generated';
import CompactContext from '../../../../../../shared/CompactContext';
import { useEntityRegistry } from '../../../../../../useEntityRegistry';
import { ANTD_GRAY } from '../../../../constants';
import { useBaseEntity } from '../../../../EntityContext';
import { FkContext } from '../utils/selectedFkContext';
const ForeignKeyContent = styled.div`
position: absolute;
display: flex;
flex-direction: column;
width: 100%;
max-height: 600px;
height: 600px;
z-index: 99999;
margin-top: -590px;
box-shadow: inset 0 7px 16px -7px ${ANTD_GRAY[5]};
`;
const EntitySidePanel = styled.div`
overflow-y: scroll;
max-height: 548px;
width: 900px;
height: 548px;
padding: 8px;
border-right: 1px solid ${ANTD_GRAY[4]};
background-color: white;
`;
const FieldBadge = styled(Badge)`
margin-left: 4px;
margin-top: 12px;
&&& .ant-badge-count {
background-color: ${ANTD_GRAY[1]};
color: ${ANTD_GRAY[9]};
border: 1px solid ${ANTD_GRAY[6]};
font-size: 12px;
font-weight: 400;
height: 22px;
cursor: pointer;
}
`;
const ConstraintSection = styled.div`
padding: 20px;
padding-top: 40px;
width: 100%;
min-height: 100%;
display: flex;
justify-content: space-between;
max-height: 548px;
min-height: 548px;
background-color: ${ANTD_GRAY[2]};
`;
const TableTitle = styled.span`
font-size: 14px;
`;
const BodyContent = styled.div`
display: flex;
flex-direction: row;
border-bottom: 1px solid ${ANTD_GRAY[4]};
`;
const HeaderContent = styled.div`
margin-top: 12px;
min-height: 40px;
font-size: 16px;
font-weight: 500;
padding-left: 12px;
border-bottom: 1px solid ${ANTD_GRAY[4]};
`;
const DatasetLink = styled(Link)`
color: ${ANTD_GRAY[9]};
font-weight: 800;
`;
const ArrowContainer = styled.div`
margin-top: 40px;
`;
export const SchemaRow = ({
children,
className,
'data-row-key': fieldPath,
}: {
children: any;
className: string;
'data-row-key': string;
}) => {
const selectedFk = useContext(FkContext);
const entityRegistry = useEntityRegistry();
const baseEntity = useBaseEntity<GetDatasetQuery>();
return (
<>
<tr className={className}>{children}</tr>
{fieldPath === selectedFk?.fieldPath && (
<ForeignKeyContent>
<HeaderContent>
Foreign Key to{' '}
<DatasetLink
to={entityRegistry.getEntityUrl(
EntityType.Dataset,
selectedFk.constraint?.foreignDataset?.urn || '',
)}
>
{selectedFk.constraint?.foreignDataset?.name}
</DatasetLink>
</HeaderContent>
<BodyContent>
<EntitySidePanel>
<CompactContext.Provider value>
{entityRegistry.renderProfile(
EntityType.Dataset,
selectedFk.constraint?.foreignDataset?.urn || '',
)}
</CompactContext.Provider>
</EntitySidePanel>
<ConstraintSection>
<div>
<TableTitle>{baseEntity.dataset?.name}</TableTitle>
{selectedFk.constraint?.sourceFields?.map((field) => (
<div>
<FieldBadge count={field?.fieldPath} />
</div>
))}
</div>
<ArrowContainer>{'--->'}</ArrowContainer>
<div>
<TableTitle>{selectedFk.constraint?.foreignDataset?.name}</TableTitle>
{selectedFk.constraint?.foreignFields?.map((field) => (
<div>
<FieldBadge count={field?.fieldPath} />
</div>
))}
</div>
</ConstraintSection>
</BodyContent>
</ForeignKeyContent>
)}
</>
);
};

View File

@ -0,0 +1,5 @@
import React from 'react';
import { ForeignKeyConstraint } from '../../../../../../../types.generated';
export const FkContext =
React.createContext<{ fieldPath: string; constraint?: ForeignKeyConstraint | null } | null>(null);

View File

@ -482,6 +482,40 @@ fragment schemaMetadataFields on SchemaMetadata {
}
}
primaryKeys
foreignKeys {
name
sourceFields {
fieldPath
}
foreignFields {
fieldPath
}
foreignDataset {
urn
name
type
origin
description
uri
platform {
name
info {
displayName
logoUrl
}
}
platformNativeType
ownership {
...ownershipFields
}
globalTags {
...globalTagsFields
}
glossaryTerms {
...glossaryTerms
}
}
}
}
fragment nonRecursiveMLModel on MLModel {

View File

@ -876,7 +876,17 @@
}
],
"primaryKeys": null,
"foreignKeysSpecs": null
"foreignKeysSpecs": null,
"foreignKeys": [{
"name": "user id",
"foreignFields": [
"urn:li:schemaField:(urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_deleted,PROD),user_id)"
],
"sourceFields": [
"urn:li:schemaField:(urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_created,PROD),user_id)"
],
"foreignDataset": "urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_deleted,PROD)"
}]
}
}
]
@ -985,21 +995,6 @@
}
},
"fields": [
{
"fieldPath": "user_id",
"jsonPath": null,
"nullable": false,
"description": {
"string": "Id of the user deleted"
},
"type": {
"type": {
"com.linkedin.pegasus2avro.schema.BooleanType": {}
}
},
"nativeDataType": "varchar(100)",
"recursive": false
},
{
"fieldPath": "user_name",
"jsonPath": null,
@ -1009,15 +1004,100 @@
},
"type": {
"type": {
"com.linkedin.pegasus2avro.schema.BooleanType": {}
"com.linkedin.pegasus2avro.schema.StringType": {}
}
},
"nativeDataType": "boolean",
"nativeDataType": "varchar(100)",
"recursive": false
},
{
"fieldPath": "timestamp",
"jsonPath": null,
"nullable": false,
"description": {
"string": "Timestamp user was deleted at"
},
"type": {
"type": {
"com.linkedin.pegasus2avro.schema.NumberType": {}
}
},
"nativeDataType": "long",
"recursive": false
},
{
"fieldPath": "user_id",
"jsonPath": null,
"nullable": false,
"description": {
"string": "Id of the user deleted"
},
"type": {
"type": {
"com.linkedin.pegasus2avro.schema.StringType": {}
}
},
"nativeDataType": "varchar(100)",
"recursive": false
},
{
"fieldPath": "browser_id",
"jsonPath": null,
"nullable": false,
"description": {
"string": "Cookie attached to identify the browser"
},
"type": {
"type": {
"com.linkedin.pegasus2avro.schema.StringType": {}
}
},
"nativeDataType": "varchar(100)",
"recursive": false
},
{
"fieldPath": "session_id",
"jsonPath": null,
"nullable": false,
"description": {
"string": "Cookie attached to identify the session"
},
"type": {
"type": {
"com.linkedin.pegasus2avro.schema.StringType": {}
}
},
"nativeDataType": "varchar(100)",
"recursive": false
},
{
"fieldPath": "deletion_reason",
"jsonPath": null,
"nullable": false,
"description": {
"string": "Why the user chose to deactivate"
},
"type": {
"type": {
"com.linkedin.pegasus2avro.schema.StringType": {}
}
},
"nativeDataType": "varchar(100)",
"recursive": false
}
],
"primaryKeys": null,
"foreignKeysSpecs": null
"primaryKeys": ["user_name"],
"foreignKeysSpecs": null,
"foreignKeys": [{
"name": "user session",
"foreignFields": [
"urn:li:schemaField:(urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_created,PROD),user_id)"
],
"sourceFields": [
"urn:li:schemaField:(urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_deleted,PROD),user_id)"
],
"foreignDataset": "urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_created,PROD)"
}]
}
}
]