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