feat(graphql,ui): Update ML system V2 UI (#12598)

This commit is contained in:
Andrew Sikowitz 2025-02-12 10:23:07 -08:00 committed by GitHub
parent 5ab2378892
commit 67a6394a37
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 516 additions and 238 deletions

View File

@ -183,9 +183,11 @@ export class DataProcessInstanceEntity implements Entity<DataProcessInstance> {
parentContainers={data.parentContainers}
parentEntities={parentEntities}
container={data.container || undefined}
duration={firstState?.durationMillis}
status={firstState?.result?.resultType}
startTime={firstState?.timestampMillis}
dataProcessInstanceProps={{
startTime: firstState?.timestampMillis,
duration: firstState?.durationMillis ?? undefined,
status: firstState?.result?.resultType ?? undefined,
}}
/>
);
};

View File

@ -39,9 +39,7 @@ export const Preview = ({
health,
parentEntities,
parentContainers,
duration,
status,
startTime,
dataProcessInstanceProps,
}: {
urn: string;
name: string;
@ -64,9 +62,11 @@ export const Preview = ({
health?: Health[] | null;
parentEntities?: Array<GeneratedEntity> | null;
parentContainers?: ParentContainersResult | null;
duration?: number | null;
status?: string | null;
startTime?: number | null;
dataProcessInstanceProps?: {
startTime?: number;
duration?: number;
status?: string;
};
}): JSX.Element => {
const entityRegistry = useEntityRegistry();
return (
@ -95,9 +95,7 @@ export const Preview = ({
paths={paths}
health={health || undefined}
parentEntities={parentEntities}
duration={duration}
status={status}
startTime={startTime}
dataProcessInstanceProps={dataProcessInstanceProps}
/>
);
};

View File

@ -2,13 +2,13 @@ import React from 'react';
import * as QueryString from 'query-string';
import { useHistory, useLocation } from 'react-router';
import { ApolloError } from '@apollo/client';
import { EntityType, FacetFilterInput } from '../../../../../../types.generated';
import { FacetFilterInput } from '../../../../../../types.generated';
import useFilters from '../../../../../search/utils/useFilters';
import { navigateToEntitySearchUrl } from './navigateToEntitySearchUrl';
import { FilterSet, GetSearchResultsParams, SearchResultsInterface } from './types';
import { useEntityQueryParams } from '../../../containers/profile/utils';
import { EmbeddedListSearch } from './EmbeddedListSearch';
import { UnionType } from '../../../../../search/utils/constants';
import { EMBEDDED_LIST_SEARCH_ENTITY_TYPES, UnionType } from '../../../../../search/utils/constants';
import {
DownloadSearchResults,
DownloadSearchResultsInput,
@ -16,30 +16,6 @@ import {
} from '../../../../../search/utils/types';
const FILTER = 'filter';
const SEARCH_ENTITY_TYPES = [
EntityType.Dataset,
EntityType.Dashboard,
EntityType.Chart,
EntityType.Mlmodel,
EntityType.MlmodelGroup,
EntityType.MlfeatureTable,
EntityType.Mlfeature,
EntityType.MlprimaryKey,
EntityType.DataFlow,
EntityType.DataJob,
EntityType.GlossaryTerm,
EntityType.GlossaryNode,
EntityType.Tag,
EntityType.Role,
EntityType.CorpUser,
EntityType.CorpGroup,
EntityType.Container,
EntityType.Domain,
EntityType.DataProduct,
EntityType.Notebook,
EntityType.BusinessAttribute,
EntityType.DataProcessInstance,
];
function getParamsWithoutFilters(params: QueryString.ParsedQuery<string>) {
const paramsCopy = { ...params };
@ -161,7 +137,7 @@ export const EmbeddedListSearchSection = ({
return (
<EmbeddedListSearch
entityTypes={SEARCH_ENTITY_TYPES}
entityTypes={EMBEDDED_LIST_SEARCH_ENTITY_TYPES}
query={query || ''}
page={page}
unionType={unionType}

View File

@ -45,6 +45,7 @@ import {
Documentation,
DisplayProperties,
VersionProperties,
DataProcessRunEvent,
} from '../../../types.generated';
import { FetchedEntity } from '../../lineage/types';
@ -135,6 +136,9 @@ export type GenericEntityProperties = {
displayProperties?: Maybe<DisplayProperties>;
notes?: Maybe<EntityRelationshipsResult>;
versionProperties?: Maybe<VersionProperties>;
// Data process instance
lastRunEvent?: Maybe<DataProcessRunEvent>;
};
export type GenericEntityUpdate = {

View File

@ -1,3 +1,4 @@
import DataProcessInstanceSummary from '@src/app/entity/dataProcessInstance/profile/DataProcessInstanceSummary';
import { GenericEntityProperties } from '@app/entity/shared/types';
import { Entity as GraphQLEntity } from '@types';
import { globalEntityRegistryV2 } from '@app/EntityRegistryProvider';
@ -14,7 +15,6 @@ import { ArrowsClockwise } from 'phosphor-react';
import React from 'react';
import { DataProcessInstance, EntityType, SearchResult } from '../../../types.generated';
import Preview from './preview/Preview';
import DataProcessInstanceSummary from './profile/DataProcessInstanceSummary';
const getParentEntities = (data: DataProcessInstance): GraphQLEntity[] => {
const parentEntity = data?.relationships?.relationships?.find(
@ -79,9 +79,7 @@ export class DataProcessInstanceEntity implements Entity<DataProcessInstance> {
useEntityQuery={this.useEntityQuery}
// useUpdateQuery={useUpdateDataProcessInstanceMutation}
getOverrideProperties={this.getOverridePropertiesFromEntity}
headerDropdownItems={
new Set([EntityMenuItems.UPDATE_DEPRECATION, EntityMenuItems.RAISE_INCIDENT, EntityMenuItems.SHARE])
}
headerDropdownItems={new Set([EntityMenuItems.SHARE])}
tabs={[
{
name: 'Summary',
@ -115,6 +113,8 @@ export class DataProcessInstanceEntity implements Entity<DataProcessInstance> {
(processInstance as GetDataProcessInstanceQuery['dataProcessInstance'])?.optionalPlatform ||
parent?.platform,
parent,
// Not currently rendered in V2
lastRunEvent: processInstance?.state?.[0],
};
};

View File

@ -1,102 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import { Space, Table, Typography } from 'antd';
import { formatDetailedDuration } from '@src/app/shared/time/timeUtils';
import { capitalize } from 'lodash';
import moment from 'moment';
import { MlHyperParam, MlMetric, DataProcessInstanceRunResultType } from '../../../../types.generated';
import { useBaseEntity } from '../../../entity/shared/EntityContext';
import { InfoItem } from '../../shared/components/styled/InfoItem';
import { GetDataProcessInstanceQuery } from '../../../../graphql/dataProcessInstance.generated';
import { Pill } from '../../../../alchemy-components/components/Pills';
const TabContent = styled.div`
padding: 16px;
`;
const InfoItemContainer = styled.div<{ justifyContent }>`
display: flex;
position: relative;
justify-content: ${(props) => props.justifyContent};
padding: 0px 2px;
`;
const InfoItemContent = styled.div`
padding-top: 8px;
width: 100px;
`;
const propertyTableColumns = [
{
title: 'Name',
dataIndex: 'name',
width: 450,
},
{
title: 'Value',
dataIndex: 'value',
},
];
export default function DataProcessInstanceSummary() {
const baseEntity = useBaseEntity<GetDataProcessInstanceQuery>();
const dpi = baseEntity?.dataProcessInstance;
const formatStatus = (state) => {
if (!state || state.length === 0) return '-';
const result = state[0]?.result?.resultType;
const statusColor = result === DataProcessInstanceRunResultType.Success ? 'green' : 'red';
return <Pill label={capitalize(result)} color={statusColor} clickable={false} />;
};
const formatDuration = (state) => {
if (!state || state.length === 0) return '-';
return formatDetailedDuration(state[0]?.durationMillis);
};
return (
<TabContent>
<Space direction="vertical" style={{ width: '100%' }} size="large">
<Typography.Title level={3}>Details</Typography.Title>
<InfoItemContainer justifyContent="left">
<InfoItem title="Created At">
<InfoItemContent>
{dpi?.properties?.created?.time
? moment(dpi.properties.created.time).format('YYYY-MM-DD HH:mm:ss')
: '-'}
</InfoItemContent>
</InfoItem>
<InfoItem title="Status">
<InfoItemContent>{formatStatus(dpi?.state)}</InfoItemContent>
</InfoItem>
<InfoItem title="Duration">
<InfoItemContent>{formatDuration(dpi?.state)}</InfoItemContent>
</InfoItem>
<InfoItem title="Run ID">
<InfoItemContent>{dpi?.mlTrainingRunProperties?.id}</InfoItemContent>
</InfoItem>
<InfoItem title="Created By">
<InfoItemContent>{dpi?.properties?.created?.actor}</InfoItemContent>
</InfoItem>
</InfoItemContainer>
<InfoItemContainer justifyContent="left">
<InfoItem title="Artifacts Location">
<InfoItemContent>{dpi?.mlTrainingRunProperties?.outputUrls}</InfoItemContent>
</InfoItem>
</InfoItemContainer>
<Typography.Title level={3}>Training Metrics</Typography.Title>
<Table
pagination={false}
columns={propertyTableColumns}
dataSource={dpi?.mlTrainingRunProperties?.trainingMetrics as MlMetric[]}
/>
<Typography.Title level={3}>Hyper Parameters</Typography.Title>
<Table
pagination={false}
columns={propertyTableColumns}
dataSource={dpi?.mlTrainingRunProperties?.hyperParams as MlHyperParam[]}
/>
</Space>
</TabContent>
);
}

View File

@ -1,4 +1,5 @@
import { CodeSandboxOutlined, UnorderedListOutlined } from '@ant-design/icons';
import { CodeSandboxOutlined, PartitionOutlined, UnorderedListOutlined } from '@ant-design/icons';
import { LineageTab } from '@app/entityV2/shared/tabs/Lineage/LineageTab';
import * as React from 'react';
import { useGetMlModelQuery } from '../../../graphql/mlModel.generated';
import { EntityType, MlModel, SearchResult } from '../../../types.generated';
@ -80,6 +81,8 @@ export class MLModelEntity implements Entity<MlModel> {
getOverridePropertiesFromEntity = (mlModel?: MlModel | null): GenericEntityProperties => {
return {
// eslint-disable-next-line @typescript-eslint/dot-notation
name: mlModel && this.displayName(mlModel),
externalUrl: mlModel?.properties?.externalUrl,
};
};
@ -103,6 +106,11 @@ export class MLModelEntity implements Entity<MlModel> {
name: 'Documentation',
component: DocumentationTab,
},
{
name: 'Lineage',
component: LineageTab,
icon: PartitionOutlined,
},
{
name: 'Properties',
component: PropertiesTab,
@ -193,8 +201,7 @@ export class MLModelEntity implements Entity<MlModel> {
getLineageVizConfig = (entity: MlModel) => {
return {
urn: entity.urn,
// eslint-disable-next-line @typescript-eslint/dot-notation
name: entity.properties?.['propertiesName'] || entity.name,
name: entity && this.displayName(entity),
type: EntityType.Mlmodel,
icon: entity.platform?.properties?.logoUrl || undefined,
platform: entity.platform,
@ -203,11 +210,16 @@ export class MLModelEntity implements Entity<MlModel> {
};
displayName = (data: MlModel) => {
return data.properties?.name || data.name || data.urn;
// eslint-disable-next-line @typescript-eslint/dot-notation
return data.properties?.['propertiesName'] || data.properties?.name || data.name || data.urn;
};
getGenericEntityProperties = (mlModel: MlModel) => {
return getDataForEntityType({ data: mlModel, entityType: this.type, getOverrideProperties: (data) => data });
return getDataForEntityType({
data: mlModel,
entityType: this.type,
getOverrideProperties: this.getOverridePropertiesFromEntity,
});
};
supportedCapabilities = () => {

View File

@ -31,8 +31,7 @@ export const Preview = ({
return (
<DefaultPreviewCard
url={entityRegistry.getEntityUrl(EntityType.Mlmodel, model.urn)}
// eslint-disable-next-line @typescript-eslint/dot-notation
name={model?.properties?.['propertiesName'] || model?.name || ''}
name={data?.name || ''}
urn={model.urn}
data={data}
description={model.description || ''}

View File

@ -1,18 +1,47 @@
import { useBaseEntity } from '@app/entity/shared/EntityContext';
import { InfoItem } from '@app/entityV2/shared/components/styled/InfoItem';
import { notEmpty } from '@app/entityV2/shared/utils';
import { useEntityRegistry } from '@app/useEntityRegistry';
import { Pill } from '@components';
import { GetMlModelQuery } from '@graphql/mlModel.generated';
import { EntityType, MlHyperParam, MlMetric } from '@types';
import React from 'react';
import styled from 'styled-components';
import { Space, Table, Typography } from 'antd';
import { MlHyperParam, MlMetric } from '../../../../types.generated';
import { useBaseEntity } from '../../../entity/shared/EntityContext';
import { GetMlModelQuery } from '../../../../graphql/mlModel.generated';
import { Link } from 'react-router-dom';
import { colors } from '@src/alchemy-components/theme';
import moment from 'moment';
const TabContent = styled.div`
padding: 16px;
`;
const InfoItemContainer = styled.div<{ justifyContent }>`
display: flex;
position: relative;
justify-content: ${(props) => props.justifyContent};
padding: 0px 2px;
`;
const InfoItemContent = styled.div`
padding-top: 8px;
width: 100px;
display: flex;
flex-wrap: wrap;
gap: 5px;
`;
const JobLink = styled(Link)`
color: ${colors.blue[700]};
&:hover {
text-decoration: underline;
}
`;
export default function MLModelSummary() {
const baseEntity = useBaseEntity<GetMlModelQuery>();
const model = baseEntity?.mlModel;
const entityRegistry = useEntityRegistry();
const propertyTableColumns = [
{
@ -26,9 +55,72 @@ export default function MLModelSummary() {
},
];
const renderTrainingJobs = () => {
const trainingJobs =
model?.trainedBy?.relationships?.map((relationship) => relationship.entity).filter(notEmpty) || [];
if (trainingJobs.length === 0) return '-';
return (
<div>
{trainingJobs.map((job, index) => {
const { urn, name } = job as { urn: string; name?: string };
return (
<span key={urn}>
<JobLink to={entityRegistry.getEntityUrl(EntityType.DataProcessInstance, urn)}>
{name || urn}
</JobLink>
{index < trainingJobs.length - 1 && ', '}
</span>
);
})}
</div>
);
};
return (
<TabContent>
<Space direction="vertical" style={{ width: '100%' }} size="large">
<Typography.Title level={3}>Model Details</Typography.Title>
<InfoItemContainer justifyContent="left">
<InfoItem title="Version">
<InfoItemContent>{model?.versionProperties?.version?.versionTag}</InfoItemContent>
</InfoItem>
<InfoItem title="Registered At">
<InfoItemContent>
{model?.properties?.created?.time
? moment(model.properties.created.time).format('YYYY-MM-DD HH:mm:ss')
: '-'}
</InfoItemContent>
</InfoItem>
<InfoItem title="Last Modified At">
<InfoItemContent>
{model?.properties?.lastModified?.time
? moment(model.properties.lastModified.time).format('YYYY-MM-DD HH:mm:ss')
: '-'}
</InfoItemContent>
</InfoItem>
<InfoItem title="Created By">
<InfoItemContent>{model?.properties?.created?.actor}</InfoItemContent>
</InfoItem>
</InfoItemContainer>
<InfoItemContainer justifyContent="left">
<InfoItem title="Aliases">
<InfoItemContent>
{model?.versionProperties?.aliases?.map((alias) => (
<Pill
label={alias.versionTag ?? '-'}
key={alias.versionTag}
color="blue"
clickable={false}
/>
))}
</InfoItemContent>
</InfoItem>
<InfoItem title="Source Run">
<InfoItemContent>{renderTrainingJobs()}</InfoItemContent>
</InfoItem>
</InfoItemContainer>
<Typography.Title level={3}>Training Metrics</Typography.Title>
<Table
pagination={false}

View File

@ -1,4 +1,5 @@
import { CodeSandboxOutlined, UnorderedListOutlined } from '@ant-design/icons';
import { CodeSandboxOutlined, PartitionOutlined, UnorderedListOutlined } from '@ant-design/icons';
import { LineageTab } from '@app/entityV2/shared/tabs/Lineage/LineageTab';
import * as React from 'react';
import { useGetMlModelGroupQuery } from '../../../graphql/mlModelGroup.generated';
import { EntityType, MlModelGroup, SearchResult } from '../../../types.generated';
@ -24,7 +25,11 @@ import { Preview } from './preview/Preview';
import ModelGroupModels from './profile/ModelGroupModels';
import SidebarNotesSection from '../shared/sidebarSection/SidebarNotesSection';
const headerDropdownItems = new Set([EntityMenuItems.UPDATE_DEPRECATION, EntityMenuItems.ANNOUNCE]);
const headerDropdownItems = new Set([
EntityMenuItems.SHARE,
EntityMenuItems.UPDATE_DEPRECATION,
EntityMenuItems.ANNOUNCE,
]);
/**
* Definition of the DataHub MlModelGroup entity.
@ -70,8 +75,10 @@ export class MLModelGroupEntity implements Entity<MlModelGroup> {
getCollectionName = () => 'ML Groups';
getOverridePropertiesFromEntity = (_?: MlModelGroup | null): GenericEntityProperties => {
return {};
getOverridePropertiesFromEntity = (mlModelGroup?: MlModelGroup | null): GenericEntityProperties => {
return {
name: mlModelGroup && this.displayName(mlModelGroup),
};
};
useEntityQuery = useGetMlModelGroupQuery;
@ -93,6 +100,11 @@ export class MLModelGroupEntity implements Entity<MlModelGroup> {
name: 'Documentation',
component: DocumentationTab,
},
{
name: 'Lineage',
component: LineageTab,
icon: PartitionOutlined,
},
{
name: 'Properties',
component: PropertiesTab,
@ -175,8 +187,7 @@ export class MLModelGroupEntity implements Entity<MlModelGroup> {
getLineageVizConfig = (entity: MlModelGroup) => {
return {
urn: entity.urn,
// eslint-disable-next-line @typescript-eslint/dot-notation
name: entity.properties?.['propertiesName'] || entity.name,
name: entity && this.displayName(entity),
type: EntityType.MlmodelGroup,
icon: entity.platform?.properties?.logoUrl || undefined,
platform: entity.platform,
@ -185,14 +196,15 @@ export class MLModelGroupEntity implements Entity<MlModelGroup> {
};
displayName = (data: MlModelGroup) => {
return data.properties?.name || data.name || data.urn;
// eslint-disable-next-line @typescript-eslint/dot-notation
return data.properties?.['propertiesName'] || data.properties?.name || data.name || data.urn;
};
getGenericEntityProperties = (mlModelGroup: MlModelGroup) => {
return getDataForEntityType({
data: mlModelGroup,
entityType: this.type,
getOverrideProperties: (data) => data,
getOverrideProperties: this.getOverridePropertiesFromEntity,
});
};

View File

@ -30,8 +30,7 @@ export const Preview = ({
return (
<DefaultPreviewCard
url={entityRegistry.getEntityUrl(EntityType.MlmodelGroup, group.urn)}
// eslint-disable-next-line @typescript-eslint/dot-notation
name={group?.properties?.['propertiesName'] || group?.name || ''}
name={data?.name || ''}
urn={group.urn}
data={data}
platformInstanceId={group.dataPlatformInstance?.instanceId}

View File

@ -1,32 +1,200 @@
import { List, Space, Typography } from 'antd';
import { useBaseEntity } from '@app/entity/shared/EntityContext';
import { EmptyTab } from '@app/entityV2/shared/components/styled/EmptyTab';
import { InfoItem } from '@app/entityV2/shared/components/styled/InfoItem';
import { notEmpty } from '@app/entityV2/shared/utils';
import { useEntityRegistry } from '@app/useEntityRegistry';
import { GetMlModelGroupQuery } from '@graphql/mlModelGroup.generated';
import { EntityType } from '@types';
import { Typography, Table } from 'antd';
import React from 'react';
import { GetMlModelGroupQuery } from '../../../../graphql/mlModelGroup.generated';
import { EntityType } from '../../../../types.generated';
import { useEntityRegistry } from '../../../useEntityRegistry';
import { PreviewType } from '../../Entity';
import { useBaseEntity } from '../../../entity/shared/EntityContext';
import styled from 'styled-components';
import { colors } from '@src/alchemy-components/theme';
import { Pill } from '@src/alchemy-components/components/Pills';
import moment from 'moment';
const InfoItemContainer = styled.div<{ justifyContent }>`
display: flex;
position: relative;
justify-content: ${(props) => props.justifyContent};
padding: 12px 2px 20px 2px;
`;
const InfoItemContent = styled.div`
padding-top: 8px;
width: 100px;
`;
const NameContainer = styled.div`
display: flex;
align-items: center;
`;
const NameLink = styled.a`
font-weight: 700;
color: inherit;
font-size: 0.9rem;
&:hover {
color: ${colors.blue[400]} !important;
}
`;
const TagContainer = styled.div`
display: inline-flex;
margin-left: 0px;
margin-top: 3px;
flex-wrap: wrap;
margin-right: 8px;
backgroundcolor: white;
gap: 5px;
`;
const StyledTable = styled(Table)`
&&& .ant-table-cell {
padding: 16px;
}
` as typeof Table;
const ModelsContainer = styled.div`
width: 100%;
padding: 20px;
`;
const VersionContainer = styled.div`
display: flex;
align-items: center;
`;
export default function MLGroupModels() {
const baseEntity = useBaseEntity<GetMlModelGroupQuery>();
const models = baseEntity?.mlModelGroup?.incoming?.relationships?.map((relationship) => relationship.entity) || [];
const entityRegistry = useEntityRegistry();
const modelGroup = baseEntity?.mlModelGroup;
const models =
baseEntity?.mlModelGroup?.incoming?.relationships
?.map((relationship) => relationship.entity)
.filter(notEmpty)
// eslint-disable-next-line @typescript-eslint/dot-notation
?.sort((a, b) => b?.['properties']?.createdTS?.time - a?.['properties']?.createdTS?.time) || [];
const columns = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
width: 300,
render: (_: any, record) => (
<NameContainer>
<NameLink href={entityRegistry.getEntityUrl(EntityType.Mlmodel, record.urn)}>
{record?.properties?.propertiesName || record?.name}
</NameLink>
</NameContainer>
),
},
{
title: 'Version',
key: 'version',
width: 70,
render: (_: any, record: any) => (
<VersionContainer>{record.versionProperties?.version?.versionTag || '-'}</VersionContainer>
),
},
{
title: 'Created At',
key: 'createdAt',
width: 150,
render: (_: any, record: any) => (
<Typography.Text>
{record.properties?.createdTS?.time
? moment(record.properties.createdTS.time).format('YYYY-MM-DD HH:mm:ss')
: '-'}
</Typography.Text>
),
},
{
title: 'Aliases',
key: 'aliases',
width: 200,
render: (_: any, record: any) => {
const aliases = record.versionProperties?.aliases || [];
return (
<TagContainer>
{aliases.map((alias) => (
<Pill key={alias.versionTag} label={alias.versionTag} color="blue" clickable={false} />
))}
</TagContainer>
);
},
},
{
title: 'Tags',
key: 'tags',
width: 200,
render: (_: any, record: any) => {
const tags = record.properties?.tags || [];
return (
<TagContainer>
{tags.map((tag) => (
<Pill key={tag} label={tag} clickable={false} />
))}
</TagContainer>
);
},
},
{
title: 'Description',
dataIndex: 'description',
key: 'description',
width: 300,
render: (_: any, record: any) => {
const editableDesc = record.editableProperties?.description;
const originalDesc = record.description;
return <Typography.Text>{editableDesc || originalDesc || '-'}</Typography.Text>;
},
},
];
return (
<>
<Space direction="vertical" style={{ width: '100%' }} size="large">
<List
style={{ padding: '16px 16px' }}
bordered
dataSource={models}
header={<Typography.Title level={3}>Models</Typography.Title>}
renderItem={(item) => (
<List.Item style={{ paddingTop: '20px' }}>
{entityRegistry.renderPreview(EntityType.Mlmodel, PreviewType.PREVIEW, item)}
</List.Item>
)}
/>
</Space>
</>
<ModelsContainer>
<Typography.Title level={3}>Model Group Details</Typography.Title>
<InfoItemContainer justifyContent="left">
<InfoItem title="Created At">
<InfoItemContent>
{modelGroup?.properties?.created?.time
? moment(modelGroup.properties.created.time).format('YYYY-MM-DD HH:mm:ss')
: '-'}
</InfoItemContent>
</InfoItem>
<InfoItem title="Last Modified At">
<InfoItemContent>
{modelGroup?.properties?.lastModified?.time
? moment(modelGroup.properties.lastModified.time).format('YYYY-MM-DD HH:mm:ss')
: '-'}
</InfoItemContent>
</InfoItem>
{modelGroup?.properties?.created?.actor && (
<InfoItem title="Created By">
<InfoItemContent>{modelGroup.properties.created?.actor}</InfoItemContent>
</InfoItem>
)}
</InfoItemContainer>
<Typography.Title level={3}>Models</Typography.Title>
<StyledTable
columns={columns}
dataSource={models}
pagination={false}
rowKey="urn"
expandable={{
defaultExpandAllRows: true,
expandRowByClick: true,
}}
locale={{
emptyText: <EmptyTab tab="mlModel" />,
}}
/>
</ModelsContainer>
);
}

View File

@ -4,13 +4,13 @@ import { useHistory, useLocation } from 'react-router';
import { ApolloError } from '@apollo/client';
import useSortInput from '@src/app/searchV2/sorting/useSortInput';
import { useSelectedSortOption } from '@src/app/search/context/SearchContext';
import { EntityType, FacetFilterInput } from '../../../../../../types.generated';
import { FacetFilterInput } from '../../../../../../types.generated';
import useFilters from '../../../../../search/utils/useFilters';
import { navigateToEntitySearchUrl } from './navigateToEntitySearchUrl';
import { FilterSet, GetSearchResultsParams, SearchResultsInterface } from './types';
import { useEntityQueryParams } from '../../../containers/profile/utils';
import { EmbeddedListSearch } from './EmbeddedListSearch';
import { UnionType } from '../../../../../search/utils/constants';
import { EMBEDDED_LIST_SEARCH_ENTITY_TYPES, UnionType } from '../../../../../search/utils/constants';
import {
DownloadSearchResults,
DownloadSearchResultsInput,
@ -19,30 +19,6 @@ import {
import { decodeComma } from '../../../utils';
const FILTER = 'filter';
const SEARCH_ENTITY_TYPES = [
EntityType.Dataset,
EntityType.Dashboard,
EntityType.Chart,
EntityType.Mlmodel,
EntityType.MlmodelGroup,
EntityType.MlfeatureTable,
EntityType.Mlfeature,
EntityType.MlprimaryKey,
EntityType.DataFlow,
EntityType.DataJob,
EntityType.GlossaryTerm,
EntityType.GlossaryNode,
EntityType.Tag,
EntityType.Role,
EntityType.CorpUser,
EntityType.CorpGroup,
EntityType.Container,
EntityType.Domain,
EntityType.DataProduct,
EntityType.Notebook,
EntityType.BusinessAttribute,
EntityType.DataProcessInstance,
];
function getParamsWithoutFilters(params: QueryString.ParsedQuery<string>) {
const paramsCopy = { ...params };
@ -170,7 +146,7 @@ export const EmbeddedListSearchSection = ({
return (
<EmbeddedListSearch
entityTypes={SEARCH_ENTITY_TYPES}
entityTypes={EMBEDDED_LIST_SEARCH_ENTITY_TYPES}
query={query || ''}
page={page}
unionType={unionType}

View File

@ -1,3 +1,4 @@
import { getContextPath } from '@app/entityV2/shared/containers/profile/header/getContextPath';
import VersioningBadge from '@app/entityV2/shared/versioning/VersioningBadge';
import { Divider } from 'antd';
import React, { useState } from 'react';
@ -7,7 +8,6 @@ import {
DataPlatform,
DisplayProperties,
Domain,
Entity,
EntityType,
Post,
} from '../../../../../../types.generated';
@ -150,14 +150,7 @@ export const DefaultEntityHeader = ({
const displayedEntityType = getDisplayedEntityType(entityData, entityRegistry, entityType);
const { platform, platforms } = getEntityPlatforms(entityType, entityData);
const containerPath =
entityData?.parentContainers?.containers ||
entityData?.parentDomains?.domains ||
entityData?.parentNodes?.nodes ||
[];
const parentPath: Entity[] = entityData?.parent ? [entityData.parent as Entity] : [];
const parentEntities = containerPath.length ? containerPath : parentPath;
const contextPath = getContextPath(entityData);
return (
<>
<Row>
@ -236,7 +229,7 @@ export const DefaultEntityHeader = ({
type={displayedEntityType}
entityType={entityType}
browsePaths={entityData?.browsePathV2}
parentEntities={parentEntities}
parentEntities={contextPath}
contentRef={contentRef}
isContentTruncated={isContentTruncated}
/>

View File

@ -0,0 +1,102 @@
import { GenericEntityProperties } from '@app/entity/shared/types';
import { dataPlatform } from '@src/Mocks';
import { EntityType } from '@types';
import { getContextPath } from './getContextPath';
const PARENT_CONTAINERS: GenericEntityProperties['parentContainers'] = {
containers: [
{
urn: 'urn:li:container:1',
type: EntityType.Container,
platform: dataPlatform,
},
{
urn: 'urn:li:container:2',
type: EntityType.Container,
platform: dataPlatform,
},
],
count: 2,
};
const PARENT_DOMAINS: GenericEntityProperties['parentDomains'] = {
domains: [
{ urn: 'urn:li:domain:1', type: EntityType.Domain },
{ urn: 'urn:li:domain:2', type: EntityType.Domain },
],
count: 2,
};
const PARENT_NODES: GenericEntityProperties['parentNodes'] = {
nodes: [
{ urn: 'urn:li:glossaryNode:1', type: EntityType.GlossaryNode },
{
urn: 'urn:li:glossaryNode:2',
type: EntityType.GlossaryNode,
},
],
count: 2,
};
const PARENT: GenericEntityProperties = {
urn: 'urn:li:dataset:(urn:li:dataPlatform:snowflake,name,PROD)',
type: EntityType.Dataset,
platform: dataPlatform,
};
describe('getContextPath', () => {
it('returns empty array by default', () => {
const entityData = {};
const contextPath = getContextPath(entityData);
expect(contextPath).toEqual([]);
});
it('returns correct context path for entity with parent containers', () => {
const entityData = {
parentContainers: PARENT_CONTAINERS,
parentDomains: PARENT_DOMAINS,
parentNodes: PARENT_NODES,
parent: PARENT,
};
const contextPath = getContextPath(entityData);
expect(contextPath).toEqual(PARENT_CONTAINERS.containers);
});
it('returns correct context path for entity with parent domains', () => {
const entityData = {
parentContainers: null,
parentDomains: PARENT_DOMAINS,
parentNodes: PARENT_NODES,
parent: PARENT,
};
const contextPath = getContextPath(entityData);
expect(contextPath).toEqual(PARENT_DOMAINS.domains);
});
it('returns correct context path for entity with parent nodes', () => {
const entityData = {
parentContainers: null,
parentDomains: null,
parentNodes: PARENT_NODES,
parent: PARENT,
};
const contextPath = getContextPath(entityData);
expect(contextPath).toEqual(PARENT_NODES.nodes);
});
it('returns correct context path for entity with parent', () => {
const entityData = {
parentContainers: null,
parentDomains: null,
parentNodes: null,
parent: PARENT,
};
const contextPath = getContextPath(entityData);
expect(contextPath).toEqual([PARENT]);
});
});

View File

@ -0,0 +1,17 @@
import { GenericEntityProperties } from '@app/entity/shared/types';
import { Entity } from '@types';
type GetContextPathInput = Pick<
GenericEntityProperties,
'parent' | 'parentContainers' | 'parentDomains' | 'parentNodes'
>;
export function getContextPath(entityData: GetContextPathInput | null): Entity[] {
const containerPath =
entityData?.parentContainers?.containers ||
entityData?.parentDomains?.domains ||
entityData?.parentNodes?.nodes ||
[];
const parentPath: Entity[] = entityData?.parent ? [entityData.parent as Entity] : [];
return containerPath.length ? containerPath : parentPath;
}

View File

@ -5,7 +5,6 @@ import {
toRelativeTimeString,
} from '@app/shared/time/timeUtils';
import { Pill, Popover } from '@components';
import { Maybe } from 'graphql/jsutils/Maybe';
import { capitalize } from 'lodash';
import React from 'react';
import styled from 'styled-components';
@ -43,9 +42,9 @@ const popoverStyles = {
};
interface Props {
startTime: Maybe<number>;
duration: Maybe<number>;
status: Maybe<string>;
startTime?: number;
duration?: number;
status?: string;
}
export default function DataProcessInstanceRightColumn({ startTime, duration, status }: Props) {

View File

@ -200,9 +200,11 @@ interface Props {
paths?: EntityPath[];
health?: Health[];
parentDataset?: Dataset;
startTime?: number | null;
duration?: number | null;
status?: string | null;
dataProcessInstanceProps?: {
startTime?: number;
duration?: number;
status?: string;
};
}
export default function DefaultPreviewCard({
@ -246,9 +248,7 @@ export default function DefaultPreviewCard({
paths,
health,
parentDataset,
startTime,
duration,
status,
dataProcessInstanceProps,
}: 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
@ -277,7 +277,11 @@ export default function DefaultPreviewCard({
};
const shouldShowRightColumn =
(topUsers && topUsers.length > 0) || (owners && owners.length > 0) || startTime || duration || status;
(topUsers && topUsers.length > 0) ||
(owners && owners.length > 0) ||
dataProcessInstanceProps?.startTime ||
dataProcessInstanceProps?.duration ||
dataProcessInstanceProps?.status;
const uniqueOwners = getUniqueOwners(owners);
return (
@ -387,7 +391,7 @@ export default function DefaultPreviewCard({
</LeftColumn>
{shouldShowRightColumn && (
<RightColumn key="right-column">
<DataProcessInstanceRightColumn startTime={startTime} duration={duration} status={status} />
<DataProcessInstanceRightColumn {...dataProcessInstanceProps} />
{topUsers && topUsers?.length > 0 && (
<>
<UserListContainer>

View File

@ -1,3 +1,5 @@
import { EntityType } from '@types';
export const FILTER_URL_PREFIX = 'filter_';
export const SEARCH_FOR_ENTITY_PREFIX = 'SEARCH__';
export const EXACT_SEARCH_PREFIX = 'EXACT__';
@ -140,3 +142,28 @@ export const FilterModes = {
export type FilterMode = (typeof FilterModes)[keyof typeof FilterModes];
export const MAX_COUNT_VAL = 10000;
export const EMBEDDED_LIST_SEARCH_ENTITY_TYPES = [
EntityType.Dataset,
EntityType.Dashboard,
EntityType.Chart,
EntityType.Mlmodel,
EntityType.MlmodelGroup,
EntityType.MlfeatureTable,
EntityType.Mlfeature,
EntityType.MlprimaryKey,
EntityType.DataFlow,
EntityType.DataJob,
EntityType.GlossaryTerm,
EntityType.GlossaryNode,
EntityType.Tag,
EntityType.Role,
EntityType.CorpUser,
EntityType.CorpGroup,
EntityType.Container,
EntityType.Domain,
EntityType.DataProduct,
EntityType.Notebook,
EntityType.BusinessAttribute,
EntityType.DataProcessInstance,
];