mirror of
https://github.com/datahub-project/datahub.git
synced 2025-08-31 12:52:13 +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.DataFetchingEnvironment;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@ -122,7 +121,14 @@ public class DatasetHealthResolver implements DataFetcher<CompletableFuture<Heal
|
||||
.stream()
|
||||
.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.
|
||||
final Health health = new Health();
|
||||
@ -141,20 +147,18 @@ public class DatasetHealthResolver implements DataFetcher<CompletableFuture<Heal
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<String> getFailingAssertionUrns(final String asserteeUrn, final Set<String> candidateAssertionUrns) {
|
||||
// Query timeseries backend
|
||||
GenericTable result = _timeseriesAspectService.getAggregatedStats(
|
||||
private GenericTable getAssertionRunsTable(final String asserteeUrn) {
|
||||
return _timeseriesAspectService.getAggregatedStats(
|
||||
Constants.ASSERTION_ENTITY_NAME,
|
||||
Constants.ASSERTION_RUN_EVENT_ASPECT_NAME,
|
||||
createAssertionAggregationSpecs(),
|
||||
createAssertionsFilter(asserteeUrn),
|
||||
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
|
||||
return resultToFailedAssertionUrns(result.getRows(), candidateAssertionUrns);
|
||||
return resultToFailedAssertionUrns(assertionRunsResult.getRows(), candidateAssertionUrns);
|
||||
}
|
||||
|
||||
private Filter createAssertionsFilter(final String datasetUrn) {
|
||||
|
@ -1,8 +1,16 @@
|
||||
import React from 'react';
|
||||
import { Tooltip } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
import { getHealthIcon } from '../../../../../shared/health/healthUtils';
|
||||
import { HealthStatus } from '../../../../../../types.generated';
|
||||
|
||||
const StatusContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-left: 8px;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
status: HealthStatus;
|
||||
message?: string | undefined;
|
||||
@ -11,8 +19,8 @@ type Props = {
|
||||
export const EntityHealthStatus = ({ status, message }: Props) => {
|
||||
const icon = getHealthIcon(status, 18);
|
||||
return (
|
||||
<div style={{ paddingLeft: 12, paddingRight: 12, paddingTop: 4, paddingBottom: 4 }}>
|
||||
<StatusContainer>
|
||||
<Tooltip title={message}>{icon}</Tooltip>
|
||||
</div>
|
||||
</StatusContainer>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Popover, Typography } from 'antd';
|
||||
import React from 'react';
|
||||
import { Popover, Typography, Button } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
AssertionStdAggregation,
|
||||
@ -10,10 +10,11 @@ import {
|
||||
SchemaFieldRef,
|
||||
} from '../../../../../../types.generated';
|
||||
import { getFormattedParameterValue } from './assertionUtils';
|
||||
import { DatasetAssertionLogicModal } from './DatasetAssertionLogicModal';
|
||||
|
||||
const SqlText = styled.pre`
|
||||
margin: 0px;
|
||||
const ViewLogicButton = styled(Button)`
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
@ -314,7 +315,7 @@ const TOOLTIP_MAX_WIDTH = 440;
|
||||
*/
|
||||
export const DatasetAssertionDescription = ({ assertionInfo }: Props) => {
|
||||
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
|
||||
*/
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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`
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
padding-left: 40px;
|
||||
padding-top: 0px;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid ${ANTD_GRAY[4.5]};
|
||||
|
@ -47,23 +47,28 @@ enum ViewType {
|
||||
export const ValidationsTab = () => {
|
||||
const { urn, entityData } = useEntityData();
|
||||
const { data, refetch } = useGetDatasetAssertionsQuery({ variables: { urn } });
|
||||
const [removedUrns, setRemovedUrns] = useState<string[]>([]);
|
||||
/**
|
||||
* Determines which view should be visible: assertions or tests.
|
||||
*/
|
||||
const [view, setView] = useState(ViewType.ASSERTIONS);
|
||||
|
||||
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 maybeFailingTests = (entityData as any)?.testResults?.failing || [];
|
||||
const totalTests = maybeFailingTests.length + passingTests.length;
|
||||
|
||||
useEffect(() => {
|
||||
if (totalAssertions === 0) {
|
||||
if (totalTests > 0 && maybeTotalAssertions === 0) {
|
||||
setView(ViewType.TESTS);
|
||||
} else {
|
||||
setView(ViewType.ASSERTIONS);
|
||||
}
|
||||
}, [totalAssertions]);
|
||||
}, [totalTests, maybeTotalAssertions]);
|
||||
|
||||
// Pre-sort the list of assertions based on which has been most recently executed.
|
||||
assertions.sort(sortAssertions);
|
||||
@ -72,9 +77,13 @@ export const ValidationsTab = () => {
|
||||
<>
|
||||
<TabToolbar>
|
||||
<div>
|
||||
<Button type="text" disabled={totalAssertions === 0} onClick={() => setView(ViewType.ASSERTIONS)}>
|
||||
<Button
|
||||
type="text"
|
||||
disabled={effectiveTotalAssertions === 0}
|
||||
onClick={() => setView(ViewType.ASSERTIONS)}
|
||||
>
|
||||
<FileProtectOutlined />
|
||||
Assertions ({totalAssertions})
|
||||
Assertions ({effectiveTotalAssertions})
|
||||
</Button>
|
||||
<Button type="text" disabled={totalTests === 0} onClick={() => setView(ViewType.TESTS)}>
|
||||
<FileDoneOutlined />
|
||||
@ -84,8 +93,17 @@ export const ValidationsTab = () => {
|
||||
</TabToolbar>
|
||||
{(view === ViewType.ASSERTIONS && (
|
||||
<>
|
||||
<DatasetAssertionsSummary summary={getAssertionsStatusSummary(assertions)} />
|
||||
{entityData && <DatasetAssertionsList assertions={assertions} onDelete={() => refetch()} />}
|
||||
<DatasetAssertionsSummary summary={getAssertionsStatusSummary(filteredAssertions)} />
|
||||
{entityData && (
|
||||
<DatasetAssertionsList
|
||||
assertions={filteredAssertions}
|
||||
onDelete={(assertionUrn) => {
|
||||
// Hack to deal with eventual consistency.
|
||||
setRemovedUrns([...removedUrns, assertionUrn]);
|
||||
setTimeout(() => refetch(), 3000);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)) || <TestResults passing={passingTests} failing={maybeFailingTests} />}
|
||||
</>
|
||||
|
Loading…
x
Reference in New Issue
Block a user