mirror of
				https://github.com/datahub-project/datahub.git
				synced 2025-11-04 04:39:10 +00:00 
			
		
		
		
	refactor(UI): Refactor Dataset Health Status (#5222)
This commit is contained in:
		
							parent
							
								
									907d5cd81d
								
							
						
					
					
						commit
						6b82a0b0b6
					
				@ -10,6 +10,7 @@ import com.linkedin.datahub.graphql.QueryContext;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.Dataset;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.Health;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.HealthStatus;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.HealthStatusType;
 | 
			
		||||
import com.linkedin.metadata.Constants;
 | 
			
		||||
import com.linkedin.metadata.graph.GraphClient;
 | 
			
		||||
import com.linkedin.metadata.query.filter.Condition;
 | 
			
		||||
@ -35,6 +36,8 @@ import java.util.concurrent.TimeUnit;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import lombok.AllArgsConstructor;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -44,34 +47,45 @@ import lombok.AllArgsConstructor;
 | 
			
		||||
 * health status will be undefined for the Dataset.
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
public class DatasetHealthResolver implements DataFetcher<CompletableFuture<Health>> {
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class DatasetHealthResolver implements DataFetcher<CompletableFuture<List<Health>>> {
 | 
			
		||||
 | 
			
		||||
  private static final String ASSERTS_RELATIONSHIP_NAME = "Asserts";
 | 
			
		||||
  private static final String ASSERTION_RUN_EVENT_SUCCESS_TYPE = "SUCCESS";
 | 
			
		||||
  private static final CachedHealth NO_HEALTH = new CachedHealth(false, null);
 | 
			
		||||
 | 
			
		||||
  private final GraphClient _graphClient;
 | 
			
		||||
  private final TimeseriesAspectService _timeseriesAspectService;
 | 
			
		||||
  private final Config _config;
 | 
			
		||||
 | 
			
		||||
  private final Cache<String, CachedHealth> _statusCache;
 | 
			
		||||
 | 
			
		||||
  public DatasetHealthResolver(final GraphClient graphClient, final TimeseriesAspectService timeseriesAspectService) {
 | 
			
		||||
  public DatasetHealthResolver(
 | 
			
		||||
      final GraphClient graphClient,
 | 
			
		||||
      final TimeseriesAspectService timeseriesAspectService) {
 | 
			
		||||
    this(graphClient, timeseriesAspectService, new Config(true));
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
  public DatasetHealthResolver(
 | 
			
		||||
      final GraphClient graphClient,
 | 
			
		||||
      final TimeseriesAspectService timeseriesAspectService,
 | 
			
		||||
      final Config config) {
 | 
			
		||||
    _graphClient = graphClient;
 | 
			
		||||
    _timeseriesAspectService = timeseriesAspectService;
 | 
			
		||||
    _statusCache = CacheBuilder.newBuilder()
 | 
			
		||||
        .maximumSize(10000)
 | 
			
		||||
        .expireAfterWrite(1, TimeUnit.MINUTES)
 | 
			
		||||
        .build();
 | 
			
		||||
    _config = config;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public CompletableFuture<Health> get(final DataFetchingEnvironment environment) throws Exception {
 | 
			
		||||
  public CompletableFuture<List<Health>> get(final DataFetchingEnvironment environment) throws Exception {
 | 
			
		||||
    final Dataset parent = environment.getSource();
 | 
			
		||||
    return CompletableFuture.supplyAsync(() -> {
 | 
			
		||||
        try {
 | 
			
		||||
          final CachedHealth cachedStatus = _statusCache.get(parent.getUrn(), () -> (
 | 
			
		||||
              computeHealthStatusForDataset(parent.getUrn(), environment.getContext())));
 | 
			
		||||
          return cachedStatus.hasStatus ? cachedStatus.health : null;
 | 
			
		||||
          return cachedStatus.healths;
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
          throw new RuntimeException("Failed to resolve dataset's health status.", e);
 | 
			
		||||
        }
 | 
			
		||||
@ -87,11 +101,15 @@ public class DatasetHealthResolver implements DataFetcher<CompletableFuture<Heal
 | 
			
		||||
   *
 | 
			
		||||
   */
 | 
			
		||||
  private CachedHealth computeHealthStatusForDataset(final String datasetUrn, final QueryContext context) {
 | 
			
		||||
    final Health result = computeAssertionHealthForDataset(datasetUrn, context);
 | 
			
		||||
    if (result == null) {
 | 
			
		||||
      return NO_HEALTH;
 | 
			
		||||
    final List<Health> healthStatuses = new ArrayList<>();
 | 
			
		||||
 | 
			
		||||
    if (_config.getAssertionsEnabled()) {
 | 
			
		||||
      final Health assertionsHealth = computeAssertionHealthForDataset(datasetUrn, context);
 | 
			
		||||
      if (assertionsHealth != null) {
 | 
			
		||||
        healthStatuses.add(assertionsHealth);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return new CachedHealth(true, result);
 | 
			
		||||
    return new CachedHealth(healthStatuses);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -132,6 +150,7 @@ public class DatasetHealthResolver implements DataFetcher<CompletableFuture<Heal
 | 
			
		||||
 | 
			
		||||
      // Finally compute & return the health.
 | 
			
		||||
      final Health health = new Health();
 | 
			
		||||
      health.setType(HealthStatusType.ASSERTIONS);
 | 
			
		||||
      if (failingAssertionUrns.size() > 0) {
 | 
			
		||||
        health.setStatus(HealthStatus.FAIL);
 | 
			
		||||
        health.setMessage(String.format("Dataset is failing %s/%s assertions.", failingAssertionUrns.size(),
 | 
			
		||||
@ -217,9 +236,14 @@ public class DatasetHealthResolver implements DataFetcher<CompletableFuture<Heal
 | 
			
		||||
    return failedAssertionUrns;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    @AllArgsConstructor
 | 
			
		||||
  @Data
 | 
			
		||||
  @AllArgsConstructor
 | 
			
		||||
  public static class Config {
 | 
			
		||||
    private Boolean assertionsEnabled;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @AllArgsConstructor
 | 
			
		||||
  private static class CachedHealth {
 | 
			
		||||
    private final boolean hasStatus;
 | 
			
		||||
    private final Health health;
 | 
			
		||||
    private final List<Health> healths;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -928,9 +928,9 @@ type Dataset implements EntityWithRelationships & Entity {
 | 
			
		||||
    lineage(input: LineageInput!): EntityLineageResult
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    Experimental! The resolved health status of the Dataset
 | 
			
		||||
    Experimental! The resolved health statuses of the Dataset
 | 
			
		||||
    """
 | 
			
		||||
    health: Health
 | 
			
		||||
    health: [Health!]
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    Schema metadata of the dataset
 | 
			
		||||
@ -1119,7 +1119,7 @@ type VersionedDataset implements Entity {
 | 
			
		||||
    """
 | 
			
		||||
    Experimental! The resolved health status of the Dataset
 | 
			
		||||
    """
 | 
			
		||||
    health: Health
 | 
			
		||||
    health: [Health!]
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    Schema metadata of the dataset
 | 
			
		||||
@ -8202,10 +8202,25 @@ enum HealthStatus {
 | 
			
		||||
  FAIL
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
The type of the health status
 | 
			
		||||
"""
 | 
			
		||||
enum HealthStatusType {
 | 
			
		||||
  """
 | 
			
		||||
  Assertions status
 | 
			
		||||
  """
 | 
			
		||||
  ASSERTIONS
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
The resolved Health of an Asset
 | 
			
		||||
"""
 | 
			
		||||
type Health {
 | 
			
		||||
  """
 | 
			
		||||
  An enum representing the type of health indicator
 | 
			
		||||
  """
 | 
			
		||||
  type: HealthStatusType!
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  An enum representing the resolved Health status of an Asset
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,7 @@ import com.linkedin.metadata.timeseries.TimeseriesAspectService;
 | 
			
		||||
import com.linkedin.timeseries.GenericTable;
 | 
			
		||||
import graphql.schema.DataFetchingEnvironment;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import org.mockito.Mockito;
 | 
			
		||||
import org.testng.annotations.Test;
 | 
			
		||||
 | 
			
		||||
@ -90,9 +91,10 @@ public class DatasetHealthResolverTest {
 | 
			
		||||
    parentDataset.setUrn(TEST_DATASET_URN);
 | 
			
		||||
    Mockito.when(mockEnv.getSource()).thenReturn(parentDataset);
 | 
			
		||||
 | 
			
		||||
    Health result = resolver.get(mockEnv).get();
 | 
			
		||||
    List<Health> result = resolver.get(mockEnv).get();
 | 
			
		||||
    assertNotNull(result);
 | 
			
		||||
    assertEquals(result.getStatus(), HealthStatus.PASS);
 | 
			
		||||
    assertEquals(result.size(), 1);
 | 
			
		||||
    assertEquals(result.get(0).getStatus(), HealthStatus.PASS);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
@ -129,8 +131,8 @@ public class DatasetHealthResolverTest {
 | 
			
		||||
    parentDataset.setUrn(TEST_DATASET_URN);
 | 
			
		||||
    Mockito.when(mockEnv.getSource()).thenReturn(parentDataset);
 | 
			
		||||
 | 
			
		||||
    Health result = resolver.get(mockEnv).get();
 | 
			
		||||
    assertNull(result);
 | 
			
		||||
    List<Health> result = resolver.get(mockEnv).get();
 | 
			
		||||
    assertEquals(result.size(), 0);
 | 
			
		||||
 | 
			
		||||
    Mockito.verify(mockAspectService, Mockito.times(0)).getAggregatedStats(
 | 
			
		||||
        Mockito.any(),
 | 
			
		||||
@ -206,8 +208,8 @@ public class DatasetHealthResolverTest {
 | 
			
		||||
    parentDataset.setUrn(TEST_DATASET_URN);
 | 
			
		||||
    Mockito.when(mockEnv.getSource()).thenReturn(parentDataset);
 | 
			
		||||
 | 
			
		||||
    Health result = resolver.get(mockEnv).get();
 | 
			
		||||
    assertNotNull(result);
 | 
			
		||||
    assertEquals(result.getStatus(), HealthStatus.FAIL);
 | 
			
		||||
    List<Health> result = resolver.get(mockEnv).get();
 | 
			
		||||
    assertEquals(result.size(), 1);
 | 
			
		||||
    assertEquals(result.get(0).getStatus(), HealthStatus.FAIL);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -207,7 +207,7 @@ export const dataset1 = {
 | 
			
		||||
    container: null,
 | 
			
		||||
    upstream: null,
 | 
			
		||||
    downstream: null,
 | 
			
		||||
    health: null,
 | 
			
		||||
    health: [],
 | 
			
		||||
    assertions: null,
 | 
			
		||||
    deprecation: null,
 | 
			
		||||
    testResults: null,
 | 
			
		||||
@ -288,7 +288,7 @@ export const dataset2 = {
 | 
			
		||||
    container: null,
 | 
			
		||||
    upstream: null,
 | 
			
		||||
    downstream: null,
 | 
			
		||||
    health: null,
 | 
			
		||||
    health: [],
 | 
			
		||||
    assertions: null,
 | 
			
		||||
    status: null,
 | 
			
		||||
    deprecation: null,
 | 
			
		||||
@ -498,7 +498,7 @@ export const dataset3 = {
 | 
			
		||||
    container: null,
 | 
			
		||||
    lineage: null,
 | 
			
		||||
    relationships: null,
 | 
			
		||||
    health: null,
 | 
			
		||||
    health: [],
 | 
			
		||||
    assertions: null,
 | 
			
		||||
    status: null,
 | 
			
		||||
    readRuns: null,
 | 
			
		||||
 | 
			
		||||
@ -192,12 +192,13 @@ export const EntityHeader = ({ refreshBrowser, headerDropdownItems, isNameEditab
 | 
			
		||||
                            </DeprecatedContainer>
 | 
			
		||||
                        </Popover>
 | 
			
		||||
                    )}
 | 
			
		||||
                    {entityData?.health && (
 | 
			
		||||
                    {entityData?.health?.map((health) => (
 | 
			
		||||
                        <EntityHealthStatus
 | 
			
		||||
                            status={entityData?.health.status}
 | 
			
		||||
                            message={entityData?.health?.message || undefined}
 | 
			
		||||
                            type={health.type}
 | 
			
		||||
                            status={health.status}
 | 
			
		||||
                            message={health.message || undefined}
 | 
			
		||||
                        />
 | 
			
		||||
                    )}
 | 
			
		||||
                    ))}
 | 
			
		||||
                </TitleWrapper>
 | 
			
		||||
                <EntityCount entityCount={entityCount} />
 | 
			
		||||
            </MainHeaderContent>
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ import React from 'react';
 | 
			
		||||
import { Tooltip } from 'antd';
 | 
			
		||||
import styled from 'styled-components';
 | 
			
		||||
import { getHealthIcon } from '../../../../../shared/health/healthUtils';
 | 
			
		||||
import { HealthStatus } from '../../../../../../types.generated';
 | 
			
		||||
import { HealthStatus, HealthStatusType } from '../../../../../../types.generated';
 | 
			
		||||
 | 
			
		||||
const StatusContainer = styled.div`
 | 
			
		||||
    display: flex;
 | 
			
		||||
@ -12,12 +12,13 @@ const StatusContainer = styled.div`
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
    type: HealthStatusType;
 | 
			
		||||
    status: HealthStatus;
 | 
			
		||||
    message?: string | undefined;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const EntityHealthStatus = ({ status, message }: Props) => {
 | 
			
		||||
    const icon = getHealthIcon(status, 18);
 | 
			
		||||
export const EntityHealthStatus = ({ type, status, message }: Props) => {
 | 
			
		||||
    const icon = getHealthIcon(type, status, 18);
 | 
			
		||||
    return (
 | 
			
		||||
        <StatusContainer>
 | 
			
		||||
            <Tooltip title={message}>{icon}</Tooltip>
 | 
			
		||||
 | 
			
		||||
@ -79,7 +79,7 @@ export type GenericEntityProperties = {
 | 
			
		||||
    subTypes?: Maybe<SubTypes>;
 | 
			
		||||
    entityCount?: number;
 | 
			
		||||
    container?: Maybe<Container>;
 | 
			
		||||
    health?: Maybe<Health>;
 | 
			
		||||
    health?: Maybe<Array<Health>>;
 | 
			
		||||
    status?: Maybe<Status>;
 | 
			
		||||
    deprecation?: Maybe<Deprecation>;
 | 
			
		||||
    parentContainers?: Maybe<ParentContainersResult>;
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { CheckOutlined, CloseOutlined, WarningOutlined } from '@ant-design/icons';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { HealthStatus } from '../../../types.generated';
 | 
			
		||||
import { HealthStatus, HealthStatusType } from '../../../types.generated';
 | 
			
		||||
 | 
			
		||||
export const getHealthColor = (status: HealthStatus) => {
 | 
			
		||||
    switch (status) {
 | 
			
		||||
@ -18,7 +18,7 @@ export const getHealthColor = (status: HealthStatus) => {
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getHealthIcon = (status: HealthStatus, fontSize: number) => {
 | 
			
		||||
export const getAssertionsHealthIcon = (status: HealthStatus, fontSize: number) => {
 | 
			
		||||
    switch (status) {
 | 
			
		||||
        case HealthStatus.Pass: {
 | 
			
		||||
            return <CheckOutlined style={{ color: getHealthColor(status), fontSize }} />;
 | 
			
		||||
@ -33,3 +33,13 @@ export const getHealthIcon = (status: HealthStatus, fontSize: number) => {
 | 
			
		||||
            throw new Error(`Unrecognized Health Status ${status} provided`);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getHealthIcon = (type: HealthStatusType, status: HealthStatus, fontSize: number) => {
 | 
			
		||||
    switch (type) {
 | 
			
		||||
        case HealthStatusType.Assertions: {
 | 
			
		||||
            return getAssertionsHealthIcon(status, fontSize);
 | 
			
		||||
        }
 | 
			
		||||
        default:
 | 
			
		||||
            throw new Error(`Unrecognized Health Status Type ${type} provided`);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -83,5 +83,6 @@ export const datasetEntity = ({
 | 
			
		||||
        previousSchemaMetadata: null,
 | 
			
		||||
        __typename: 'Dataset',
 | 
			
		||||
        subTypes: null,
 | 
			
		||||
        health: [],
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -127,6 +127,7 @@ fragment nonSiblingDatasetFields on Dataset {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    health {
 | 
			
		||||
        type
 | 
			
		||||
        status
 | 
			
		||||
        message
 | 
			
		||||
        causes
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,8 @@ This file documents any backwards-incompatible changes in DataHub and assists pe
 | 
			
		||||
 | 
			
		||||
### Breaking Changes
 | 
			
		||||
 | 
			
		||||
- Refactored the `health` field of the `Dataset` GraphQL Type to be of type **list of HealthStatus** (was type **HealthStatus**). See [this PR](https://github.com/datahub-project/datahub/pull/5222/files) for more details.
 | 
			
		||||
 | 
			
		||||
### Potential Downtime
 | 
			
		||||
 | 
			
		||||
### Deprecations
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user