mirror of
https://github.com/datahub-project/datahub.git
synced 2025-09-03 06:13:14 +00:00
refactor(ui): Misc improvements to Dataset Assertions UI (#5155)
This commit is contained in:
parent
76d35dcffe
commit
7c46437280
@ -28,7 +28,6 @@ import com.linkedin.timeseries.GroupingBucketType;
|
|||||||
import graphql.schema.DataFetcher;
|
import graphql.schema.DataFetcher;
|
||||||
import graphql.schema.DataFetchingEnvironment;
|
import graphql.schema.DataFetchingEnvironment;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
@ -122,7 +121,14 @@ public class DatasetHealthResolver implements DataFetcher<CompletableFuture<Heal
|
|||||||
.stream()
|
.stream()
|
||||||
.map(relationship -> relationship.getEntity().toString()).collect(Collectors.toSet());
|
.map(relationship -> relationship.getEntity().toString()).collect(Collectors.toSet());
|
||||||
|
|
||||||
final List<String> failingAssertionUrns = getFailingAssertionUrns(datasetUrn, activeAssertionUrns);
|
final GenericTable assertionRunResults = getAssertionRunsTable(datasetUrn);
|
||||||
|
|
||||||
|
if (!assertionRunResults.hasRows() || assertionRunResults.getRows().size() == 0) {
|
||||||
|
// No assertion run results found. Return empty health!
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<String> failingAssertionUrns = getFailingAssertionUrns(assertionRunResults, activeAssertionUrns);
|
||||||
|
|
||||||
// Finally compute & return the health.
|
// Finally compute & return the health.
|
||||||
final Health health = new Health();
|
final Health health = new Health();
|
||||||
@ -141,20 +147,18 @@ public class DatasetHealthResolver implements DataFetcher<CompletableFuture<Heal
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> getFailingAssertionUrns(final String asserteeUrn, final Set<String> candidateAssertionUrns) {
|
private GenericTable getAssertionRunsTable(final String asserteeUrn) {
|
||||||
// Query timeseries backend
|
return _timeseriesAspectService.getAggregatedStats(
|
||||||
GenericTable result = _timeseriesAspectService.getAggregatedStats(
|
|
||||||
Constants.ASSERTION_ENTITY_NAME,
|
Constants.ASSERTION_ENTITY_NAME,
|
||||||
Constants.ASSERTION_RUN_EVENT_ASPECT_NAME,
|
Constants.ASSERTION_RUN_EVENT_ASPECT_NAME,
|
||||||
createAssertionAggregationSpecs(),
|
createAssertionAggregationSpecs(),
|
||||||
createAssertionsFilter(asserteeUrn),
|
createAssertionsFilter(asserteeUrn),
|
||||||
createAssertionGroupingBuckets());
|
createAssertionGroupingBuckets());
|
||||||
if (!result.hasRows()) {
|
}
|
||||||
// No completed assertion runs found. Return empty list.
|
|
||||||
return Collections.emptyList();
|
private List<String> getFailingAssertionUrns(final GenericTable assertionRunsResult, final Set<String> candidateAssertionUrns) {
|
||||||
}
|
|
||||||
// Create the buckets based on the result
|
// Create the buckets based on the result
|
||||||
return resultToFailedAssertionUrns(result.getRows(), candidateAssertionUrns);
|
return resultToFailedAssertionUrns(assertionRunsResult.getRows(), candidateAssertionUrns);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Filter createAssertionsFilter(final String datasetUrn) {
|
private Filter createAssertionsFilter(final String datasetUrn) {
|
||||||
|
@ -1,8 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Tooltip } from 'antd';
|
import { Tooltip } from 'antd';
|
||||||
|
import styled from 'styled-components';
|
||||||
import { getHealthIcon } from '../../../../../shared/health/healthUtils';
|
import { getHealthIcon } from '../../../../../shared/health/healthUtils';
|
||||||
import { HealthStatus } from '../../../../../../types.generated';
|
import { HealthStatus } from '../../../../../../types.generated';
|
||||||
|
|
||||||
|
const StatusContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
status: HealthStatus;
|
status: HealthStatus;
|
||||||
message?: string | undefined;
|
message?: string | undefined;
|
||||||
@ -11,8 +19,8 @@ type Props = {
|
|||||||
export const EntityHealthStatus = ({ status, message }: Props) => {
|
export const EntityHealthStatus = ({ status, message }: Props) => {
|
||||||
const icon = getHealthIcon(status, 18);
|
const icon = getHealthIcon(status, 18);
|
||||||
return (
|
return (
|
||||||
<div style={{ paddingLeft: 12, paddingRight: 12, paddingTop: 4, paddingBottom: 4 }}>
|
<StatusContainer>
|
||||||
<Tooltip title={message}>{icon}</Tooltip>
|
<Tooltip title={message}>{icon}</Tooltip>
|
||||||
</div>
|
</StatusContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Popover, Typography } from 'antd';
|
import { Popover, Typography, Button } from 'antd';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import {
|
import {
|
||||||
AssertionStdAggregation,
|
AssertionStdAggregation,
|
||||||
@ -10,10 +10,11 @@ import {
|
|||||||
SchemaFieldRef,
|
SchemaFieldRef,
|
||||||
} from '../../../../../../types.generated';
|
} from '../../../../../../types.generated';
|
||||||
import { getFormattedParameterValue } from './assertionUtils';
|
import { getFormattedParameterValue } from './assertionUtils';
|
||||||
|
import { DatasetAssertionLogicModal } from './DatasetAssertionLogicModal';
|
||||||
|
|
||||||
const SqlText = styled.pre`
|
const ViewLogicButton = styled(Button)`
|
||||||
margin: 0px;
|
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -314,7 +315,7 @@ const TOOLTIP_MAX_WIDTH = 440;
|
|||||||
*/
|
*/
|
||||||
export const DatasetAssertionDescription = ({ assertionInfo }: Props) => {
|
export const DatasetAssertionDescription = ({ assertionInfo }: Props) => {
|
||||||
const { scope, aggregation, fields, operator, parameters, nativeType, nativeParameters, logic } = assertionInfo;
|
const { scope, aggregation, fields, operator, parameters, nativeType, nativeParameters, logic } = assertionInfo;
|
||||||
|
const [isLogicVisible, setIsLogicVisible] = useState(false);
|
||||||
/**
|
/**
|
||||||
* Build a description component from a) input (aggregation, inputs) b) the operator text
|
* Build a description component from a) input (aggregation, inputs) b) the operator text
|
||||||
*/
|
*/
|
||||||
@ -342,7 +343,19 @@ export const DatasetAssertionDescription = ({ assertionInfo }: Props) => {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div>{(logic && <SqlText>{logic}</SqlText>) || description}</div>
|
<div>{description}</div>
|
||||||
|
{logic && (
|
||||||
|
<div>
|
||||||
|
<ViewLogicButton onClick={() => setIsLogicVisible(true)} type="link">
|
||||||
|
View Logic
|
||||||
|
</ViewLogicButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<DatasetAssertionLogicModal
|
||||||
|
logic={logic || 'N/A'}
|
||||||
|
visible={isLogicVisible}
|
||||||
|
onClose={() => setIsLogicVisible(false)}
|
||||||
|
/>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
import { Modal, Button } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import Query from '../Queries/Query';
|
||||||
|
|
||||||
|
export type AssertionsSummary = {
|
||||||
|
totalAssertions: number;
|
||||||
|
totalRuns: number;
|
||||||
|
failedRuns: number;
|
||||||
|
succeededRuns: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
logic: string;
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DatasetAssertionLogicModal = ({ logic, visible, onClose }: Props) => {
|
||||||
|
return (
|
||||||
|
<Modal visible={visible} onCancel={onClose} footer={<Button onClick={onClose}>Close</Button>}>
|
||||||
|
<Query query={logic} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
@ -6,9 +6,9 @@ import { ANTD_GRAY } from '../../../constants';
|
|||||||
|
|
||||||
const SummaryHeader = styled.div`
|
const SummaryHeader = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 80px;
|
|
||||||
padding-left: 40px;
|
padding-left: 40px;
|
||||||
padding-top: 0px;
|
padding-top: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-bottom: 1px solid ${ANTD_GRAY[4.5]};
|
border-bottom: 1px solid ${ANTD_GRAY[4.5]};
|
||||||
|
@ -47,23 +47,28 @@ enum ViewType {
|
|||||||
export const ValidationsTab = () => {
|
export const ValidationsTab = () => {
|
||||||
const { urn, entityData } = useEntityData();
|
const { urn, entityData } = useEntityData();
|
||||||
const { data, refetch } = useGetDatasetAssertionsQuery({ variables: { urn } });
|
const { data, refetch } = useGetDatasetAssertionsQuery({ variables: { urn } });
|
||||||
|
const [removedUrns, setRemovedUrns] = useState<string[]>([]);
|
||||||
/**
|
/**
|
||||||
* Determines which view should be visible: assertions or tests.
|
* Determines which view should be visible: assertions or tests.
|
||||||
*/
|
*/
|
||||||
const [view, setView] = useState(ViewType.ASSERTIONS);
|
const [view, setView] = useState(ViewType.ASSERTIONS);
|
||||||
|
|
||||||
const assertions = (data && data.dataset?.assertions?.assertions?.map((assertion) => assertion as Assertion)) || [];
|
const assertions = (data && data.dataset?.assertions?.assertions?.map((assertion) => assertion as Assertion)) || [];
|
||||||
const totalAssertions = data?.dataset?.assertions?.total || 0;
|
const maybeTotalAssertions = data?.dataset?.assertions?.total || undefined;
|
||||||
|
const effectiveTotalAssertions = maybeTotalAssertions || 0;
|
||||||
|
const filteredAssertions = assertions.filter((assertion) => !removedUrns.includes(assertion.urn));
|
||||||
|
|
||||||
const passingTests = (entityData as any)?.testResults?.passing || [];
|
const passingTests = (entityData as any)?.testResults?.passing || [];
|
||||||
const maybeFailingTests = (entityData as any)?.testResults?.failing || [];
|
const maybeFailingTests = (entityData as any)?.testResults?.failing || [];
|
||||||
const totalTests = maybeFailingTests.length + passingTests.length;
|
const totalTests = maybeFailingTests.length + passingTests.length;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (totalAssertions === 0) {
|
if (totalTests > 0 && maybeTotalAssertions === 0) {
|
||||||
setView(ViewType.TESTS);
|
setView(ViewType.TESTS);
|
||||||
|
} else {
|
||||||
|
setView(ViewType.ASSERTIONS);
|
||||||
}
|
}
|
||||||
}, [totalAssertions]);
|
}, [totalTests, maybeTotalAssertions]);
|
||||||
|
|
||||||
// Pre-sort the list of assertions based on which has been most recently executed.
|
// Pre-sort the list of assertions based on which has been most recently executed.
|
||||||
assertions.sort(sortAssertions);
|
assertions.sort(sortAssertions);
|
||||||
@ -72,9 +77,13 @@ export const ValidationsTab = () => {
|
|||||||
<>
|
<>
|
||||||
<TabToolbar>
|
<TabToolbar>
|
||||||
<div>
|
<div>
|
||||||
<Button type="text" disabled={totalAssertions === 0} onClick={() => setView(ViewType.ASSERTIONS)}>
|
<Button
|
||||||
|
type="text"
|
||||||
|
disabled={effectiveTotalAssertions === 0}
|
||||||
|
onClick={() => setView(ViewType.ASSERTIONS)}
|
||||||
|
>
|
||||||
<FileProtectOutlined />
|
<FileProtectOutlined />
|
||||||
Assertions ({totalAssertions})
|
Assertions ({effectiveTotalAssertions})
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="text" disabled={totalTests === 0} onClick={() => setView(ViewType.TESTS)}>
|
<Button type="text" disabled={totalTests === 0} onClick={() => setView(ViewType.TESTS)}>
|
||||||
<FileDoneOutlined />
|
<FileDoneOutlined />
|
||||||
@ -84,8 +93,17 @@ export const ValidationsTab = () => {
|
|||||||
</TabToolbar>
|
</TabToolbar>
|
||||||
{(view === ViewType.ASSERTIONS && (
|
{(view === ViewType.ASSERTIONS && (
|
||||||
<>
|
<>
|
||||||
<DatasetAssertionsSummary summary={getAssertionsStatusSummary(assertions)} />
|
<DatasetAssertionsSummary summary={getAssertionsStatusSummary(filteredAssertions)} />
|
||||||
{entityData && <DatasetAssertionsList assertions={assertions} onDelete={() => refetch()} />}
|
{entityData && (
|
||||||
|
<DatasetAssertionsList
|
||||||
|
assertions={filteredAssertions}
|
||||||
|
onDelete={(assertionUrn) => {
|
||||||
|
// Hack to deal with eventual consistency.
|
||||||
|
setRemovedUrns([...removedUrns, assertionUrn]);
|
||||||
|
setTimeout(() => refetch(), 3000);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)) || <TestResults passing={passingTests} failing={maybeFailingTests} />}
|
)) || <TestResults passing={passingTests} failing={maybeFailingTests} />}
|
||||||
</>
|
</>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user