mirror of
https://github.com/datahub-project/datahub.git
synced 2025-11-02 03:39:03 +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