mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-12 18:47:45 +00:00
feat(graphql,ui): Update ML system V2 UI (#12598)
This commit is contained in:
parent
5ab2378892
commit
67a6394a37
@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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],
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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 = () => {
|
||||
|
||||
@ -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 || ''}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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]);
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user