refactor(UI): Refactor Dataset Health Status (#5222)

This commit is contained in:
John Joyce 2022-06-22 15:21:34 -04:00 committed by GitHub
parent 907d5cd81d
commit 6b82a0b0b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 92 additions and 35 deletions

View File

@ -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;
}
}

View File

@ -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
"""

View File

@ -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);
}
}

View File

@ -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,

View File

@ -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>

View File

@ -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>

View File

@ -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>;

View File

@ -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`);
}
};

View File

@ -83,5 +83,6 @@ export const datasetEntity = ({
previousSchemaMetadata: null,
__typename: 'Dataset',
subTypes: null,
health: [],
};
};

View File

@ -127,6 +127,7 @@ fragment nonSiblingDatasetFields on Dataset {
}
}
health {
type
status
message
causes

View File

@ -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