mirror of
https://github.com/datahub-project/datahub.git
synced 2025-08-14 20:26:53 +00:00
feat: update ml system UI (#12334)
Co-authored-by: Andrew Sikowitz <andrew.sikowitz@acryl.io> Co-authored-by: RyanHolstien <RyanHolstien@users.noreply.github.com> Co-authored-by: Shirshanka Das <shirshanka@apache.org> Co-authored-by: ryota-cloud <ryota.egashira@acryl.io>
This commit is contained in:
parent
dbd57c972f
commit
47134c272b
@ -3,8 +3,11 @@ package com.linkedin.datahub.graphql.types.mlmodel.mappers;
|
||||
import com.linkedin.common.urn.Urn;
|
||||
import com.linkedin.datahub.graphql.QueryContext;
|
||||
import com.linkedin.datahub.graphql.generated.MLModelGroupProperties;
|
||||
import com.linkedin.datahub.graphql.generated.MLModelLineageInfo;
|
||||
import com.linkedin.datahub.graphql.types.common.mappers.CustomPropertiesMapper;
|
||||
import com.linkedin.datahub.graphql.types.common.mappers.TimeStampToAuditStampMapper;
|
||||
import com.linkedin.datahub.graphql.types.mappers.EmbeddedModelMapper;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@ -33,10 +36,40 @@ public class MLModelGroupPropertiesMapper
|
||||
result.setVersion(VersionTagMapper.map(context, mlModelGroupProperties.getVersion()));
|
||||
}
|
||||
result.setCreatedAt(mlModelGroupProperties.getCreatedAt());
|
||||
if (mlModelGroupProperties.hasCreated()) {
|
||||
result.setCreated(
|
||||
TimeStampToAuditStampMapper.map(context, mlModelGroupProperties.getCreated()));
|
||||
}
|
||||
if (mlModelGroupProperties.getName() != null) {
|
||||
result.setName(mlModelGroupProperties.getName());
|
||||
} else {
|
||||
// backfill name from URN for backwards compatibility
|
||||
result.setName(entityUrn.getEntityKey().get(1)); // indexed access is safe here
|
||||
}
|
||||
|
||||
if (mlModelGroupProperties.hasLastModified()) {
|
||||
result.setLastModified(
|
||||
TimeStampToAuditStampMapper.map(context, mlModelGroupProperties.getLastModified()));
|
||||
}
|
||||
|
||||
result.setCustomProperties(
|
||||
CustomPropertiesMapper.map(mlModelGroupProperties.getCustomProperties(), entityUrn));
|
||||
|
||||
final MLModelLineageInfo lineageInfo = new MLModelLineageInfo();
|
||||
if (mlModelGroupProperties.hasTrainingJobs()) {
|
||||
lineageInfo.setTrainingJobs(
|
||||
mlModelGroupProperties.getTrainingJobs().stream()
|
||||
.map(urn -> urn.toString())
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
if (mlModelGroupProperties.hasDownstreamJobs()) {
|
||||
lineageInfo.setDownstreamJobs(
|
||||
mlModelGroupProperties.getDownstreamJobs().stream()
|
||||
.map(urn -> urn.toString())
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
result.setMlModelLineageInfo(lineageInfo);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import static com.linkedin.datahub.graphql.authorization.AuthorizationUtils.canV
|
||||
import com.linkedin.common.urn.Urn;
|
||||
import com.linkedin.datahub.graphql.QueryContext;
|
||||
import com.linkedin.datahub.graphql.generated.MLModelGroup;
|
||||
import com.linkedin.datahub.graphql.generated.MLModelLineageInfo;
|
||||
import com.linkedin.datahub.graphql.generated.MLModelProperties;
|
||||
import com.linkedin.datahub.graphql.types.common.mappers.CustomPropertiesMapper;
|
||||
import com.linkedin.datahub.graphql.types.common.mappers.TimeStampToAuditStampMapper;
|
||||
@ -87,6 +88,20 @@ public class MLModelPropertiesMapper
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
result.setTags(mlModelProperties.getTags());
|
||||
final MLModelLineageInfo lineageInfo = new MLModelLineageInfo();
|
||||
if (mlModelProperties.hasTrainingJobs()) {
|
||||
lineageInfo.setTrainingJobs(
|
||||
mlModelProperties.getTrainingJobs().stream()
|
||||
.map(urn -> urn.toString())
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
if (mlModelProperties.hasDownstreamJobs()) {
|
||||
lineageInfo.setDownstreamJobs(
|
||||
mlModelProperties.getDownstreamJobs().stream()
|
||||
.map(urn -> urn.toString())
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
result.setMlModelLineageInfo(lineageInfo);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -25,3 +25,32 @@ input LineageEdge {
|
||||
"""
|
||||
upstreamUrn: String!
|
||||
}
|
||||
|
||||
"""
|
||||
Represents lineage information for ML entities.
|
||||
"""
|
||||
type MLModelLineageInfo {
|
||||
"""
|
||||
List of jobs or processes used to train the model.
|
||||
"""
|
||||
trainingJobs: [String!]
|
||||
|
||||
"""
|
||||
List of jobs or processes that use this model.
|
||||
"""
|
||||
downstreamJobs: [String!]
|
||||
}
|
||||
|
||||
extend type MLModelProperties {
|
||||
"""
|
||||
Information related to lineage to this model group
|
||||
"""
|
||||
mlModelLineageInfo: MLModelLineageInfo
|
||||
}
|
||||
|
||||
extend type MLModelGroupProperties {
|
||||
"""
|
||||
Information related to lineage to this model group
|
||||
"""
|
||||
mlModelLineageInfo: MLModelLineageInfo
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package com.linkedin.datahub.graphql.types.mlmodel.mappers;
|
||||
|
||||
import static org.testng.Assert.assertEquals;
|
||||
import static org.testng.Assert.assertNotNull;
|
||||
import static org.testng.Assert.assertNull;
|
||||
|
||||
import com.linkedin.common.urn.Urn;
|
||||
import com.linkedin.ml.metadata.MLModelGroupProperties;
|
||||
import java.net.URISyntaxException;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
public class MLModelGroupPropertiesMapperTest {
|
||||
|
||||
@Test
|
||||
public void testMapMLModelGroupProperties() throws URISyntaxException {
|
||||
// Create backend ML Model Group Properties
|
||||
MLModelGroupProperties input = new MLModelGroupProperties();
|
||||
|
||||
// Set description
|
||||
input.setDescription("a ml trust model group");
|
||||
|
||||
// Set Name
|
||||
input.setName("ML trust model group");
|
||||
|
||||
// Create URN
|
||||
Urn groupUrn =
|
||||
Urn.createFromString(
|
||||
"urn:li:mlModelGroup:(urn:li:dataPlatform:sagemaker,another-group,PROD)");
|
||||
|
||||
// Map the properties
|
||||
com.linkedin.datahub.graphql.generated.MLModelGroupProperties result =
|
||||
MLModelGroupPropertiesMapper.map(null, input, groupUrn);
|
||||
|
||||
// Verify mapped properties
|
||||
assertNotNull(result);
|
||||
assertEquals(result.getDescription(), "a ml trust model group");
|
||||
assertEquals(result.getName(), "ML trust model group");
|
||||
|
||||
// Verify lineage info is null as in the mock data
|
||||
assertNotNull(result.getMlModelLineageInfo());
|
||||
assertNull(result.getMlModelLineageInfo().getTrainingJobs());
|
||||
assertNull(result.getMlModelLineageInfo().getDownstreamJobs());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMapWithMinimalProperties() throws URISyntaxException {
|
||||
// Create backend ML Model Group Properties with minimal information
|
||||
MLModelGroupProperties input = new MLModelGroupProperties();
|
||||
|
||||
// Create URN
|
||||
Urn groupUrn =
|
||||
Urn.createFromString(
|
||||
"urn:li:mlModelGroup:(urn:li:dataPlatform:sagemaker,another-group,PROD)");
|
||||
|
||||
// Map the properties
|
||||
com.linkedin.datahub.graphql.generated.MLModelGroupProperties result =
|
||||
MLModelGroupPropertiesMapper.map(null, input, groupUrn);
|
||||
|
||||
// Verify basic mapping with minimal properties
|
||||
assertNotNull(result);
|
||||
assertNull(result.getDescription());
|
||||
|
||||
// Verify lineage info is null
|
||||
assertNotNull(result.getMlModelLineageInfo());
|
||||
assertNull(result.getMlModelLineageInfo().getTrainingJobs());
|
||||
assertNull(result.getMlModelLineageInfo().getDownstreamJobs());
|
||||
}
|
||||
}
|
@ -0,0 +1,187 @@
|
||||
package com.linkedin.datahub.graphql.types.mlmodel.mappers;
|
||||
|
||||
import static org.testng.Assert.assertEquals;
|
||||
import static org.testng.Assert.assertNotNull;
|
||||
import static org.testng.Assert.assertNull;
|
||||
|
||||
import com.linkedin.common.MLFeatureUrnArray;
|
||||
import com.linkedin.common.TimeStamp;
|
||||
import com.linkedin.common.VersionTag;
|
||||
import com.linkedin.common.url.Url;
|
||||
import com.linkedin.common.urn.MLFeatureUrn;
|
||||
import com.linkedin.common.urn.MLModelUrn;
|
||||
import com.linkedin.common.urn.Urn;
|
||||
import com.linkedin.data.template.StringArray;
|
||||
import com.linkedin.data.template.StringMap;
|
||||
import com.linkedin.ml.metadata.MLHyperParam;
|
||||
import com.linkedin.ml.metadata.MLHyperParamArray;
|
||||
import com.linkedin.ml.metadata.MLMetric;
|
||||
import com.linkedin.ml.metadata.MLMetricArray;
|
||||
import com.linkedin.ml.metadata.MLModelProperties;
|
||||
import java.net.URISyntaxException;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
public class MLModelPropertiesMapperTest {
|
||||
|
||||
@Test
|
||||
public void testMapMLModelProperties() throws URISyntaxException {
|
||||
MLModelProperties input = new MLModelProperties();
|
||||
|
||||
// Set basic properties
|
||||
input.setName("TestModel");
|
||||
input.setDescription("A test ML model");
|
||||
input.setType("Classification");
|
||||
|
||||
// Set version
|
||||
VersionTag versionTag = new VersionTag();
|
||||
versionTag.setVersionTag("1.0.0");
|
||||
input.setVersion(versionTag);
|
||||
|
||||
// Set external URL
|
||||
Url externalUrl = new Url("https://example.com/model");
|
||||
input.setExternalUrl(externalUrl);
|
||||
|
||||
// Set created and last modified timestamps
|
||||
TimeStamp createdTimeStamp = new TimeStamp();
|
||||
createdTimeStamp.setTime(1000L);
|
||||
Urn userUrn = Urn.createFromString("urn:li:corpuser:test");
|
||||
createdTimeStamp.setActor(userUrn);
|
||||
input.setCreated(createdTimeStamp);
|
||||
|
||||
TimeStamp lastModifiedTimeStamp = new TimeStamp();
|
||||
lastModifiedTimeStamp.setTime(2000L);
|
||||
lastModifiedTimeStamp.setActor(userUrn);
|
||||
input.setLastModified(lastModifiedTimeStamp);
|
||||
|
||||
// Set custom properties
|
||||
StringMap customProps = new StringMap();
|
||||
customProps.put("key1", "value1");
|
||||
customProps.put("key2", "value2");
|
||||
input.setCustomProperties(customProps);
|
||||
|
||||
// Set hyper parameters
|
||||
MLHyperParamArray hyperParams = new MLHyperParamArray();
|
||||
MLHyperParam hyperParam1 = new MLHyperParam();
|
||||
hyperParam1.setName("learning_rate");
|
||||
hyperParam1.setValue("0.01");
|
||||
hyperParams.add(hyperParam1);
|
||||
input.setHyperParams(hyperParams);
|
||||
|
||||
// Set training metrics
|
||||
MLMetricArray trainingMetrics = new MLMetricArray();
|
||||
MLMetric metric1 = new MLMetric();
|
||||
metric1.setName("accuracy");
|
||||
metric1.setValue("0.95");
|
||||
trainingMetrics.add(metric1);
|
||||
input.setTrainingMetrics(trainingMetrics);
|
||||
|
||||
// Set ML features
|
||||
MLFeatureUrnArray mlFeatures = new MLFeatureUrnArray();
|
||||
MLFeatureUrn featureUrn = MLFeatureUrn.createFromString("urn:li:mlFeature:(dataset,feature)");
|
||||
mlFeatures.add(featureUrn);
|
||||
input.setMlFeatures(mlFeatures);
|
||||
|
||||
// Set tags
|
||||
StringArray tags = new StringArray();
|
||||
tags.add("tag1");
|
||||
tags.add("tag2");
|
||||
input.setTags(tags);
|
||||
|
||||
// Set training and downstream jobs
|
||||
input.setTrainingJobs(
|
||||
new com.linkedin.common.UrnArray(Urn.createFromString("urn:li:dataJob:train")));
|
||||
input.setDownstreamJobs(
|
||||
new com.linkedin.common.UrnArray(Urn.createFromString("urn:li:dataJob:predict")));
|
||||
|
||||
// Create ML Model URN
|
||||
MLModelUrn modelUrn =
|
||||
MLModelUrn.createFromString(
|
||||
"urn:li:mlModel:(urn:li:dataPlatform:sagemaker,unittestmodel,PROD)");
|
||||
|
||||
// Map the properties
|
||||
com.linkedin.datahub.graphql.generated.MLModelProperties result =
|
||||
MLModelPropertiesMapper.map(null, input, modelUrn);
|
||||
|
||||
// Verify mapped properties
|
||||
assertNotNull(result);
|
||||
assertEquals(result.getName(), "TestModel");
|
||||
assertEquals(result.getDescription(), "A test ML model");
|
||||
assertEquals(result.getType(), "Classification");
|
||||
assertEquals(result.getVersion(), "1.0.0");
|
||||
assertEquals(result.getExternalUrl(), "https://example.com/model");
|
||||
|
||||
// Verify audit stamps
|
||||
assertNotNull(result.getCreated());
|
||||
assertEquals(result.getCreated().getTime().longValue(), 1000L);
|
||||
assertEquals(result.getCreated().getActor(), userUrn.toString());
|
||||
|
||||
assertNotNull(result.getLastModified());
|
||||
assertEquals(result.getLastModified().getTime().longValue(), 2000L);
|
||||
assertEquals(result.getLastModified().getActor(), userUrn.toString());
|
||||
|
||||
// Verify custom properties
|
||||
assertNotNull(result.getCustomProperties());
|
||||
|
||||
// Verify hyper parameters
|
||||
assertNotNull(result.getHyperParams());
|
||||
assertEquals(result.getHyperParams().size(), 1);
|
||||
assertEquals(result.getHyperParams().get(0).getName(), "learning_rate");
|
||||
assertEquals(result.getHyperParams().get(0).getValue(), "0.01");
|
||||
|
||||
// Verify training metrics
|
||||
assertNotNull(result.getTrainingMetrics());
|
||||
assertEquals(result.getTrainingMetrics().size(), 1);
|
||||
assertEquals(result.getTrainingMetrics().get(0).getName(), "accuracy");
|
||||
assertEquals(result.getTrainingMetrics().get(0).getValue(), "0.95");
|
||||
|
||||
// Verify ML features
|
||||
assertNotNull(result.getMlFeatures());
|
||||
assertEquals(result.getMlFeatures().size(), 1);
|
||||
assertEquals(result.getMlFeatures().get(0), featureUrn.toString());
|
||||
|
||||
// Verify tags
|
||||
assertNotNull(result.getTags());
|
||||
assertEquals(result.getTags().get(0), "tag1");
|
||||
assertEquals(result.getTags().get(1), "tag2");
|
||||
|
||||
// Verify lineage info
|
||||
assertNotNull(result.getMlModelLineageInfo());
|
||||
assertEquals(result.getMlModelLineageInfo().getTrainingJobs().size(), 1);
|
||||
assertEquals(result.getMlModelLineageInfo().getTrainingJobs().get(0), "urn:li:dataJob:train");
|
||||
assertEquals(result.getMlModelLineageInfo().getDownstreamJobs().size(), 1);
|
||||
assertEquals(
|
||||
result.getMlModelLineageInfo().getDownstreamJobs().get(0), "urn:li:dataJob:predict");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMapWithMissingName() throws URISyntaxException {
|
||||
MLModelProperties input = new MLModelProperties();
|
||||
MLModelUrn modelUrn =
|
||||
MLModelUrn.createFromString(
|
||||
"urn:li:mlModel:(urn:li:dataPlatform:sagemaker,missingnamemodel,PROD)");
|
||||
|
||||
com.linkedin.datahub.graphql.generated.MLModelProperties result =
|
||||
MLModelPropertiesMapper.map(null, input, modelUrn);
|
||||
|
||||
// Verify that name is extracted from URN when not present in input
|
||||
assertEquals(result.getName(), "missingnamemodel");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMapWithMinimalProperties() throws URISyntaxException {
|
||||
MLModelProperties input = new MLModelProperties();
|
||||
MLModelUrn modelUrn =
|
||||
MLModelUrn.createFromString(
|
||||
"urn:li:mlModel:(urn:li:dataPlatform:sagemaker,minimalmodel,PROD)");
|
||||
|
||||
com.linkedin.datahub.graphql.generated.MLModelProperties result =
|
||||
MLModelPropertiesMapper.map(null, input, modelUrn);
|
||||
|
||||
// Verify basic mapping with minimal properties
|
||||
assertNotNull(result);
|
||||
assertEquals(result.getName(), "minimalmodel");
|
||||
assertNull(result.getDescription());
|
||||
assertNull(result.getType());
|
||||
assertNull(result.getVersion());
|
||||
}
|
||||
}
|
@ -66,6 +66,7 @@ export const EntityPage = ({ entityType }: Props) => {
|
||||
entityType === EntityType.MlfeatureTable ||
|
||||
entityType === EntityType.MlmodelGroup ||
|
||||
entityType === EntityType.GlossaryTerm ||
|
||||
entityType === EntityType.DataProcessInstance ||
|
||||
entityType === EntityType.GlossaryNode;
|
||||
|
||||
return (
|
||||
|
@ -1,12 +1,7 @@
|
||||
import React from 'react';
|
||||
import { ApiOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
DataProcessInstance,
|
||||
Entity as GeneratedEntity,
|
||||
EntityType,
|
||||
OwnershipType,
|
||||
SearchResult,
|
||||
} from '../../../types.generated';
|
||||
import { Entity as GraphQLEntity } from '@types';
|
||||
import { DataProcessInstance, EntityType, OwnershipType, SearchResult } from '../../../types.generated';
|
||||
import { Preview } from './preview/Preview';
|
||||
import { Entity, EntityCapabilityType, IconStyleType, PreviewType } from '../Entity';
|
||||
import { EntityProfile } from '../shared/containers/profile/EntityProfile';
|
||||
@ -23,32 +18,21 @@ import { EntityMenuItems } from '../shared/EntityDropdown/EntityDropdown';
|
||||
import { capitalizeFirstLetterOnly } from '../../shared/textUtil';
|
||||
import DataProductSection from '../shared/containers/profile/sidebar/DataProduct/DataProductSection';
|
||||
import { getDataProduct } from '../shared/utils';
|
||||
// import SummaryTab from './profile/DataProcessInstaceSummary';
|
||||
import SummaryTab from './profile/DataProcessInstanceSummary';
|
||||
|
||||
// const getProcessPlatformName = (data?: DataProcessInstance): string => {
|
||||
// return (
|
||||
// data?.dataPlatformInstance?.platform?.properties?.displayName ||
|
||||
// capitalizeFirstLetterOnly(data?.dataPlatformInstance?.platform?.name) ||
|
||||
// ''
|
||||
// );
|
||||
// };
|
||||
|
||||
const getParentEntities = (data: DataProcessInstance): GeneratedEntity[] => {
|
||||
const getParentEntities = (data: DataProcessInstance): GraphQLEntity[] => {
|
||||
const parentEntity = data?.relationships?.relationships?.find(
|
||||
(rel) => rel.type === 'InstanceOf' && rel.entity?.type === EntityType.DataJob,
|
||||
);
|
||||
|
||||
if (!parentEntity?.entity) return [];
|
||||
if (!parentEntity || !parentEntity.entity) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Convert to GeneratedEntity
|
||||
return [
|
||||
{
|
||||
type: parentEntity.entity.type,
|
||||
urn: (parentEntity.entity as any).urn, // Make sure urn exists
|
||||
relationships: (parentEntity.entity as any).relationships,
|
||||
},
|
||||
];
|
||||
// First cast to unknown, then to Entity with proper type
|
||||
return [parentEntity.entity];
|
||||
};
|
||||
|
||||
/**
|
||||
* Definition of the DataHub DataProcessInstance entity.
|
||||
*/
|
||||
@ -97,18 +81,13 @@ export class DataProcessInstanceEntity implements Entity<DataProcessInstance> {
|
||||
urn={urn}
|
||||
entityType={EntityType.DataProcessInstance}
|
||||
useEntityQuery={this.useEntityQuery}
|
||||
// useUpdateQuery={useUpdateDataProcessInstanceMutation}
|
||||
getOverrideProperties={this.getOverridePropertiesFromEntity}
|
||||
headerDropdownItems={new Set([EntityMenuItems.UPDATE_DEPRECATION, EntityMenuItems.RAISE_INCIDENT])}
|
||||
tabs={[
|
||||
// {
|
||||
// name: 'Documentation',
|
||||
// component: DocumentationTab,
|
||||
// },
|
||||
// {
|
||||
// name: 'Summary',
|
||||
// component: SummaryTab,
|
||||
// },
|
||||
{
|
||||
name: 'Summary',
|
||||
component: SummaryTab,
|
||||
},
|
||||
{
|
||||
name: 'Lineage',
|
||||
component: LineageTab,
|
||||
@ -117,14 +96,6 @@ export class DataProcessInstanceEntity implements Entity<DataProcessInstance> {
|
||||
name: 'Properties',
|
||||
component: PropertiesTab,
|
||||
},
|
||||
// {
|
||||
// name: 'Incidents',
|
||||
// component: IncidentTab,
|
||||
// getDynamicName: (_, processInstance) => {
|
||||
// const activeIncidentCount = processInstance?.dataProcessInstance?.activeIncidents.total;
|
||||
// return `Incidents${(activeIncidentCount && ` (${activeIncidentCount})`) || ''}`;
|
||||
// },
|
||||
// },
|
||||
]}
|
||||
sidebarSections={this.getSidebarSections()}
|
||||
/>
|
||||
@ -181,13 +152,11 @@ export class DataProcessInstanceEntity implements Entity<DataProcessInstance> {
|
||||
platformLogo={data?.dataPlatformInstance?.platform?.properties?.logoUrl}
|
||||
owners={null}
|
||||
globalTags={null}
|
||||
// domain={data.domain?.domain}
|
||||
dataProduct={getDataProduct(genericProperties?.dataProduct)}
|
||||
externalUrl={data.properties?.externalUrl}
|
||||
parentContainers={data.parentContainers}
|
||||
parentEntities={parentEntities}
|
||||
container={data.container || undefined}
|
||||
// health={data.health}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -196,6 +165,9 @@ export class DataProcessInstanceEntity implements Entity<DataProcessInstance> {
|
||||
const data = result.entity as DataProcessInstance;
|
||||
const genericProperties = this.getGenericEntityProperties(data);
|
||||
const parentEntities = getParentEntities(data);
|
||||
|
||||
const firstState = data?.state && data.state.length > 0 ? data.state[0] : undefined;
|
||||
|
||||
return (
|
||||
<Preview
|
||||
urn={data.urn}
|
||||
@ -210,9 +182,7 @@ export class DataProcessInstanceEntity implements Entity<DataProcessInstance> {
|
||||
platformInstanceId={data.dataPlatformInstance?.instanceId}
|
||||
owners={null}
|
||||
globalTags={null}
|
||||
// domain={data.domain?.domain}
|
||||
dataProduct={getDataProduct(genericProperties?.dataProduct)}
|
||||
// deprecation={data.deprecation}
|
||||
insights={result.insights}
|
||||
externalUrl={data.properties?.externalUrl}
|
||||
degree={(result as any).degree}
|
||||
@ -220,10 +190,9 @@ export class DataProcessInstanceEntity implements Entity<DataProcessInstance> {
|
||||
parentContainers={data.parentContainers}
|
||||
parentEntities={parentEntities}
|
||||
container={data.container || undefined}
|
||||
// duration={data?.state?.[0]?.durationMillis}
|
||||
// status={data?.state?.[0]?.result?.resultType}
|
||||
// startTime={data?.state?.[0]?.timestampMillis}
|
||||
// health={data.health}
|
||||
duration={firstState?.durationMillis}
|
||||
status={firstState?.result?.resultType}
|
||||
startTime={firstState?.timestampMillis}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -237,7 +206,6 @@ export class DataProcessInstanceEntity implements Entity<DataProcessInstance> {
|
||||
icon: entity?.dataPlatformInstance?.platform?.properties?.logoUrl || undefined,
|
||||
platform: entity?.dataPlatformInstance?.platform,
|
||||
container: entity?.container,
|
||||
// health: entity?.health || undefined,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -39,10 +39,10 @@ export const Preview = ({
|
||||
health,
|
||||
parentEntities,
|
||||
parentContainers,
|
||||
}: // duration,
|
||||
// status,
|
||||
// startTime,
|
||||
{
|
||||
duration,
|
||||
status,
|
||||
startTime,
|
||||
}: {
|
||||
urn: string;
|
||||
name: string;
|
||||
subType?: string | null;
|
||||
@ -64,9 +64,9 @@ export const Preview = ({
|
||||
health?: Health[] | null;
|
||||
parentEntities?: Array<GeneratedEntity> | null;
|
||||
parentContainers?: ParentContainersResult | null;
|
||||
// duration?: number | null;
|
||||
// status?: string | null;
|
||||
// startTime?: number | null;
|
||||
duration?: number | null;
|
||||
status?: string | null;
|
||||
startTime?: number | null;
|
||||
}): JSX.Element => {
|
||||
const entityRegistry = useEntityRegistry();
|
||||
return (
|
||||
@ -95,9 +95,9 @@ export const Preview = ({
|
||||
paths={paths}
|
||||
health={health || undefined}
|
||||
parentEntities={parentEntities}
|
||||
// duration={duration}
|
||||
// status={status}
|
||||
// startTime={startTime}
|
||||
duration={duration}
|
||||
status={status}
|
||||
startTime={startTime}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,102 @@
|
||||
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 '../../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 MLModelSummary() {
|
||||
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)} colorScheme={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>
|
||||
);
|
||||
}
|
@ -151,7 +151,7 @@ export class MLModelEntity implements Entity<MlModel> {
|
||||
};
|
||||
|
||||
displayName = (data: MlModel) => {
|
||||
return data.name || data.urn;
|
||||
return data.properties?.name || data.name || data.urn;
|
||||
};
|
||||
|
||||
getGenericEntityProperties = (mlModel: MlModel) => {
|
||||
|
@ -21,7 +21,8 @@ export const Preview = ({
|
||||
return (
|
||||
<DefaultPreviewCard
|
||||
url={entityRegistry.getEntityUrl(EntityType.Mlmodel, model.urn)}
|
||||
name={model.name || ''}
|
||||
// eslint-disable-next-line @typescript-eslint/dot-notation
|
||||
name={model.properties?.['propertiesName'] || model.name || ''}
|
||||
urn={model.urn}
|
||||
description={model.description || ''}
|
||||
platformInstanceId={model.dataPlatformInstance?.instanceId}
|
||||
|
@ -1,18 +1,47 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Space, Table, Typography } from 'antd';
|
||||
|
||||
import { MlHyperParam, MlMetric } from '../../../../types.generated';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { colors } from '@src/alchemy-components/theme';
|
||||
import moment from 'moment';
|
||||
import { useEntityRegistry } from '../../../useEntityRegistry';
|
||||
import { MlHyperParam, MlMetric, EntityType } from '../../../../types.generated';
|
||||
import { useBaseEntity } from '../../shared/EntityContext';
|
||||
import { GetMlModelQuery } from '../../../../graphql/mlModel.generated';
|
||||
import { InfoItem } from '../../shared/components/styled/InfoItem';
|
||||
import { notEmpty } from '../../shared/utils';
|
||||
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;
|
||||
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}
|
||||
colorScheme="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}
|
||||
|
@ -139,7 +139,7 @@ export class MLModelGroupEntity implements Entity<MlModelGroup> {
|
||||
};
|
||||
|
||||
displayName = (data: MlModelGroup) => {
|
||||
return data.name || data.urn;
|
||||
return data.properties?.name || data.name || data.urn;
|
||||
};
|
||||
|
||||
getGenericEntityProperties = (mlModelGroup: MlModelGroup) => {
|
||||
|
@ -19,7 +19,8 @@ export const Preview = ({
|
||||
return (
|
||||
<DefaultPreviewCard
|
||||
url={entityRegistry.getEntityUrl(EntityType.MlmodelGroup, group.urn)}
|
||||
name={group?.name || ''}
|
||||
// eslint-disable-next-line @typescript-eslint/dot-notation
|
||||
name={group?.properties?.['propertiesName'] || group?.name || ''}
|
||||
urn={group.urn}
|
||||
platformInstanceId={group.dataPlatformInstance?.instanceId}
|
||||
description={group?.description || ''}
|
||||
|
@ -1,32 +1,202 @@
|
||||
import { List, Space, Typography } from 'antd';
|
||||
import { Typography, Table } from 'antd';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { colors } from '@src/alchemy-components/theme';
|
||||
import { Pill } from '@src/alchemy-components/components/Pills';
|
||||
import moment from 'moment';
|
||||
import { GetMlModelGroupQuery } from '../../../../graphql/mlModelGroup.generated';
|
||||
import { EntityType } from '../../../../types.generated';
|
||||
import { useEntityRegistry } from '../../../useEntityRegistry';
|
||||
import { PreviewType } from '../../Entity';
|
||||
import { useBaseEntity } from '../../shared/EntityContext';
|
||||
import { notEmpty } from '../../shared/utils';
|
||||
import { EmptyTab } from '../../shared/components/styled/EmptyTab';
|
||||
import { InfoItem } from '../../shared/components/styled/InfoItem';
|
||||
|
||||
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) || [];
|
||||
|
||||
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}
|
||||
colorScheme="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>
|
||||
);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import * as QueryString from 'query-string';
|
||||
import { useHistory, useLocation } from 'react-router';
|
||||
import { ApolloError } from '@apollo/client';
|
||||
import { FacetFilterInput } from '../../../../../../types.generated';
|
||||
import { EntityType, FacetFilterInput } from '../../../../../../types.generated';
|
||||
import useFilters from '../../../../../search/utils/useFilters';
|
||||
import { navigateToEntitySearchUrl } from './navigateToEntitySearchUrl';
|
||||
import { FilterSet, GetSearchResultsParams, SearchResultsInterface } from './types';
|
||||
@ -16,6 +16,30 @@ 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 };
|
||||
@ -137,6 +161,7 @@ export const EmbeddedListSearchSection = ({
|
||||
|
||||
return (
|
||||
<EmbeddedListSearch
|
||||
entityTypes={SEARCH_ENTITY_TYPES}
|
||||
query={query || ''}
|
||||
page={page}
|
||||
unionType={unionType}
|
||||
|
@ -82,6 +82,10 @@ export const EMPTY_MESSAGES = {
|
||||
title: 'No business attributes added yet',
|
||||
description: 'Add business attributes to entities to classify their data.',
|
||||
},
|
||||
mlModel: {
|
||||
title: 'No ML models',
|
||||
description: 'ML models will appear here if they are associated with this ML model group.',
|
||||
},
|
||||
};
|
||||
|
||||
export const ELASTIC_MAX_COUNT = 10000;
|
||||
|
@ -0,0 +1,87 @@
|
||||
import {
|
||||
formatDetailedDuration,
|
||||
formatDuration,
|
||||
toLocalDateTimeString,
|
||||
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';
|
||||
import colors from '@src/alchemy-components/theme/foundations/colors';
|
||||
import { DataProcessInstanceRunResultType } from '../../types.generated';
|
||||
|
||||
const StatContainer = styled.div`
|
||||
display: flex;
|
||||
margin-top: 40px;
|
||||
height: 25px;
|
||||
padding-left: 20px;
|
||||
color: ${colors.gray[500]};
|
||||
width: 150px;
|
||||
`;
|
||||
|
||||
const PopoverContent = styled.div`
|
||||
color: ${colors.gray[500]};
|
||||
font-size: 0.8rem;
|
||||
`;
|
||||
|
||||
const Title = styled.div`
|
||||
color: ${colors.gray[500]};
|
||||
border-bottom: none;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
const popoverStyles = {
|
||||
overlayInnerStyle: {
|
||||
borderRadius: '10px',
|
||||
},
|
||||
overlayStyle: {
|
||||
margin: '5px',
|
||||
},
|
||||
};
|
||||
|
||||
interface Props {
|
||||
startTime: Maybe<number>;
|
||||
duration: Maybe<number>;
|
||||
status: Maybe<string>;
|
||||
}
|
||||
|
||||
export default function DataProcessInstanceRightColumn({ startTime, duration, status }: Props) {
|
||||
const statusPillColor = status === DataProcessInstanceRunResultType.Success ? 'green' : 'red';
|
||||
|
||||
return (
|
||||
<>
|
||||
{startTime && (
|
||||
<Popover
|
||||
content={<PopoverContent>{toLocalDateTimeString(startTime)}</PopoverContent>}
|
||||
title={<Title>Start Time</Title>}
|
||||
trigger="hover"
|
||||
overlayInnerStyle={popoverStyles.overlayInnerStyle}
|
||||
overlayStyle={popoverStyles.overlayStyle}
|
||||
>
|
||||
<StatContainer>{toRelativeTimeString(startTime)}</StatContainer>
|
||||
</Popover>
|
||||
)}
|
||||
{duration && (
|
||||
<Popover
|
||||
content={<PopoverContent>{formatDetailedDuration(duration)}</PopoverContent>}
|
||||
title={<Title>Duration</Title>}
|
||||
trigger="hover"
|
||||
overlayInnerStyle={popoverStyles.overlayInnerStyle}
|
||||
overlayStyle={popoverStyles.overlayStyle}
|
||||
>
|
||||
<StatContainer>{formatDuration(duration)}</StatContainer>
|
||||
</Popover>
|
||||
)}
|
||||
{status && (
|
||||
<>
|
||||
<StatContainer>
|
||||
<Pill label={capitalize(status)} colorScheme={statusPillColor} clickable={false} />
|
||||
</StatContainer>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import DataProcessInstanceRightColumn from '@app/preview/DataProcessInstanceRightColumn';
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import { Divider, Tooltip, Typography } from 'antd';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
GlobalTags,
|
||||
Owner,
|
||||
@ -200,6 +200,9 @@ interface Props {
|
||||
paths?: EntityPath[];
|
||||
health?: Health[];
|
||||
parentDataset?: Dataset;
|
||||
startTime?: number | null;
|
||||
duration?: number | null;
|
||||
status?: string | null;
|
||||
}
|
||||
|
||||
export default function DefaultPreviewCard({
|
||||
@ -243,6 +246,9 @@ export default function DefaultPreviewCard({
|
||||
paths,
|
||||
health,
|
||||
parentDataset,
|
||||
startTime,
|
||||
duration,
|
||||
status,
|
||||
}: 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
|
||||
@ -270,7 +276,8 @@ export default function DefaultPreviewCard({
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
const shouldShowRightColumn = (topUsers && topUsers.length > 0) || (owners && owners.length > 0);
|
||||
const shouldShowRightColumn =
|
||||
(topUsers && topUsers.length > 0) || (owners && owners.length > 0) || startTime || duration || status;
|
||||
const uniqueOwners = getUniqueOwners(owners);
|
||||
|
||||
return (
|
||||
@ -380,6 +387,7 @@ export default function DefaultPreviewCard({
|
||||
</LeftColumn>
|
||||
{shouldShowRightColumn && (
|
||||
<RightColumn key="right-column">
|
||||
<DataProcessInstanceRightColumn startTime={startTime} duration={duration} status={status} />
|
||||
{topUsers && topUsers?.length > 0 && (
|
||||
<>
|
||||
<UserListContainer>
|
||||
|
@ -206,3 +206,41 @@ export function getTimeRangeDescription(startDate: moment.Moment | null, endDate
|
||||
|
||||
return 'Unknown time range';
|
||||
}
|
||||
|
||||
export function formatDuration(durationMs: number): string {
|
||||
const duration = moment.duration(durationMs);
|
||||
const hours = Math.floor(duration.asHours());
|
||||
const minutes = duration.minutes();
|
||||
const seconds = duration.seconds();
|
||||
|
||||
if (hours === 0 && minutes === 0) {
|
||||
return `${seconds} secs`;
|
||||
}
|
||||
|
||||
if (hours === 0) {
|
||||
return minutes === 1 ? `${minutes} min` : `${minutes} mins`;
|
||||
}
|
||||
|
||||
const minuteStr = minutes > 0 ? ` ${minutes} mins` : '';
|
||||
return hours === 1 ? `${hours} hr${minuteStr}` : `${hours} hrs${minuteStr}`;
|
||||
}
|
||||
|
||||
export function formatDetailedDuration(durationMs: number): string {
|
||||
const duration = moment.duration(durationMs);
|
||||
const hours = Math.floor(duration.asHours());
|
||||
const minutes = duration.minutes();
|
||||
const seconds = duration.seconds();
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
if (hours > 0) {
|
||||
parts.push(hours === 1 ? `${hours} hr` : `${hours} hrs`);
|
||||
}
|
||||
if (minutes > 0) {
|
||||
parts.push(minutes === 1 ? `${minutes} min` : `${minutes} mins`);
|
||||
}
|
||||
if (seconds > 0) {
|
||||
parts.push(`${seconds} secs`);
|
||||
}
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
@ -897,6 +897,10 @@ fragment nonRecursiveMLModel on MLModel {
|
||||
key
|
||||
value
|
||||
}
|
||||
mlModelLineageInfo {
|
||||
trainingJobs
|
||||
downstreamJobs
|
||||
}
|
||||
}
|
||||
globalTags {
|
||||
...globalTagsFields
|
||||
@ -971,6 +975,14 @@ fragment nonRecursiveMLModelGroupFields on MLModelGroup {
|
||||
time
|
||||
actor
|
||||
}
|
||||
lastModified {
|
||||
time
|
||||
actor
|
||||
}
|
||||
mlModelLineageInfo {
|
||||
trainingJobs
|
||||
downstreamJobs
|
||||
}
|
||||
}
|
||||
browsePathV2 {
|
||||
...browsePathV2Fields
|
||||
|
@ -272,6 +272,7 @@ fragment lineageNodeProperties on EntityWithRelationships {
|
||||
removed
|
||||
}
|
||||
properties {
|
||||
propertiesName: name
|
||||
createdTS: created {
|
||||
time
|
||||
actor
|
||||
@ -296,6 +297,9 @@ fragment lineageNodeProperties on EntityWithRelationships {
|
||||
name
|
||||
description
|
||||
origin
|
||||
tags {
|
||||
...globalTagsFields
|
||||
}
|
||||
platform {
|
||||
...platformFields
|
||||
}
|
||||
@ -305,6 +309,34 @@ fragment lineageNodeProperties on EntityWithRelationships {
|
||||
status {
|
||||
removed
|
||||
}
|
||||
versionProperties {
|
||||
versionSet {
|
||||
urn
|
||||
type
|
||||
}
|
||||
version {
|
||||
versionTag
|
||||
}
|
||||
aliases {
|
||||
versionTag
|
||||
}
|
||||
comment
|
||||
}
|
||||
properties {
|
||||
propertiesName: name
|
||||
createdTS: created {
|
||||
time
|
||||
actor
|
||||
}
|
||||
tags
|
||||
customProperties {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
editableProperties {
|
||||
description
|
||||
}
|
||||
structuredProperties {
|
||||
properties {
|
||||
...structuredPropertiesFields
|
||||
|
@ -20,6 +20,23 @@ query getMLModel($urn: String!) {
|
||||
}
|
||||
}
|
||||
}
|
||||
trainedBy: relationships(input: { types: ["TrainedBy"], direction: OUTGOING, start: 0, count: 100 }) {
|
||||
start
|
||||
count
|
||||
total
|
||||
relationships {
|
||||
type
|
||||
direction
|
||||
entity {
|
||||
... on DataProcessInstance {
|
||||
urn
|
||||
name
|
||||
type
|
||||
...dataProcessInstanceFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
privileges {
|
||||
...entityPrivileges
|
||||
}
|
||||
|
@ -886,6 +886,9 @@ fragment searchResultsWithoutSchemaField on Entity {
|
||||
...structuredPropertiesFields
|
||||
}
|
||||
}
|
||||
properties {
|
||||
propertiesName: name
|
||||
}
|
||||
}
|
||||
... on MLModelGroup {
|
||||
name
|
||||
@ -908,6 +911,9 @@ fragment searchResultsWithoutSchemaField on Entity {
|
||||
...structuredPropertiesFields
|
||||
}
|
||||
}
|
||||
properties {
|
||||
propertiesName: name
|
||||
}
|
||||
}
|
||||
... on Tag {
|
||||
name
|
||||
@ -954,6 +960,9 @@ fragment searchResultsWithoutSchemaField on Entity {
|
||||
...versionProperties
|
||||
}
|
||||
}
|
||||
... on DataProcessInstance {
|
||||
...dataProcessInstanceFields
|
||||
}
|
||||
... on DataPlatformInstance {
|
||||
...dataPlatformInstanceFields
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user