Minor: lineage data quality failure tracing support (#18580)

This commit is contained in:
Shailesh Parmar 2024-11-11 15:21:29 +05:30 committed by GitHub
parent a6d97b67a8
commit eeb27c3cbf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 192 additions and 10 deletions

View File

@ -1,4 +1,5 @@
import { DateRangeObject } from 'Models'; import { DateRangeObject } from 'Models';
import { LinkProps } from 'react-router-dom';
import { TestCaseStatus } from '../../generated/tests/testCase'; import { TestCaseStatus } from '../../generated/tests/testCase';
import { TestCaseResolutionStatusTypes } from '../../generated/tests/testCaseResolutionStatus'; import { TestCaseResolutionStatusTypes } from '../../generated/tests/testCaseResolutionStatus';
import { TestPlatform } from '../../generated/tests/testDefinition'; import { TestPlatform } from '../../generated/tests/testDefinition';
@ -76,7 +77,7 @@ export interface DataStatisticWidgetProps {
icon: SvgComponent; icon: SvgComponent;
dataLabel: string; dataLabel: string;
countValue: number; countValue: number;
redirectPath: string; redirectPath: LinkProps['to'];
linkLabel: string; linkLabel: string;
isLoading?: boolean; isLoading?: boolean;
} }

View File

@ -19,6 +19,7 @@ import { EdgeProps, getBezierPath } from 'reactflow';
import { ReactComponent as FunctionIcon } from '../../../assets/svg/ic-function.svg'; import { ReactComponent as FunctionIcon } from '../../../assets/svg/ic-function.svg';
import { ReactComponent as IconTimesCircle } from '../../../assets/svg/ic-times-circle.svg'; import { ReactComponent as IconTimesCircle } from '../../../assets/svg/ic-times-circle.svg';
import { ReactComponent as PipelineIcon } from '../../../assets/svg/pipeline-grey.svg'; import { ReactComponent as PipelineIcon } from '../../../assets/svg/pipeline-grey.svg';
import { RED_3 } from '../../../constants/Color.constants';
import { FOREIGN_OBJECT_SIZE } from '../../../constants/Lineage.constants'; import { FOREIGN_OBJECT_SIZE } from '../../../constants/Lineage.constants';
import { useLineageProvider } from '../../../context/LineageProvider/LineageProvider'; import { useLineageProvider } from '../../../context/LineageProvider/LineageProvider';
import { EntityType } from '../../../enums/entity.enum'; import { EntityType } from '../../../enums/entity.enum';
@ -76,6 +77,7 @@ export const CustomEdge = ({
isPipelineRootNode, isPipelineRootNode,
...rest ...rest
} = data; } = data;
const offset = 4; const offset = 4;
const { fromEntity, toEntity, pipeline, pipelineEntityType } = const { fromEntity, toEntity, pipeline, pipelineEntityType } =
@ -150,10 +152,16 @@ export const CustomEdge = ({
opacity = tracedNodes.length === 0 || isStrokeNeeded ? 1 : 0.25; opacity = tracedNodes.length === 0 || isStrokeNeeded ? 1 : 0.25;
} }
let stroke = isStrokeNeeded ? theme.primaryColor : undefined;
if (edge?.isDqTestFailure) {
stroke = RED_3;
}
return { return {
...style, ...style,
...{ ...{
stroke: isStrokeNeeded ? theme.primaryColor : undefined, stroke,
opacity, opacity,
}, },
}; };

View File

@ -49,6 +49,7 @@ const CustomNodeV1 = (props: NodeProps) => {
} = useLineageProvider(); } = useLineageProvider();
const { label, isNewNode, node = {}, isRootNode } = data; const { label, isNewNode, node = {}, isRootNode } = data;
const nodeType = isEditMode ? EntityLineageNodeType.DEFAULT : type; const nodeType = isEditMode ? EntityLineageNodeType.DEFAULT : type;
const isSelected = selectedNode === node; const isSelected = selectedNode === node;
const { id, lineage, fullyQualifiedName } = node; const { id, lineage, fullyQualifiedName } = node;
@ -257,6 +258,7 @@ const CustomNodeV1 = (props: NodeProps) => {
className={classNames( className={classNames(
'lineage-node p-0', 'lineage-node p-0',
isSelected ? 'custom-node-header-active' : 'custom-node-header-normal', isSelected ? 'custom-node-header-active' : 'custom-node-header-normal',
{ 'data-quality-failed-custom-node-header': node?.isDqTestFailure },
{ 'custom-node-header-tracing': isTraced } { 'custom-node-header-tracing': isTraced }
)} )}
data-testid={`lineage-node-${fullyQualifiedName}`}> data-testid={`lineage-node-${fullyQualifiedName}`}>

View File

@ -154,6 +154,41 @@
} }
} }
.react-flow__node-default.selectable.selected,
.react-flow__node-default.selectable:focus,
.react-flow__node-default.selectable:focus-visible,
.react-flow__node-input.selectable.selected,
.react-flow__node-input.selectable:focus,
.react-flow__node-input.selectable:focus-visible,
.react-flow__node-output.selectable.selected,
.react-flow__node-output.selectable:focus,
.react-flow__node-output.selectable:focus-visible,
.react-flow__node-group.selectable.selected,
.react-flow__node-group.selectable:focus,
.react-flow__node-group.selectable:focus-visible,
.react-flow__node-load-more.selectable.selected,
.react-flow__node-load-more.selectable:focus,
.react-flow__node-load-more.selectable:focus-visible {
.data-quality-failed-custom-node-header {
&.lineage-node {
border-color: @red-3 !important;
}
.lineage-node-handle {
border-color: @red-3;
svg {
color: @red-3;
}
}
.label-container {
background: fade(@red-3, 10%);
}
.column-container {
background: fade(@red-3, 10%);
border-top: 1px solid @border-color;
}
}
}
.custom-node-header-active { .custom-node-header-active {
.label-container { .label-container {
background: @primary-1; background: @primary-1;
@ -163,6 +198,15 @@
border-top: 1px solid @border-color; border-top: 1px solid @border-color;
} }
} }
.data-quality-failed-custom-node-header.custom-node-header-active {
.label-container {
background: fade(@red-3, 10%);
}
.column-container {
background: fade(@red-3, 10%);
border-top: 1px solid @red-3;
}
}
.react-flow__handle.connectable { .react-flow__handle.connectable {
background-color: @text-color; background-color: @text-color;

View File

@ -44,6 +44,25 @@
} }
} }
} }
.data-quality-failed-custom-node-header {
&.custom-node-header-normal {
border: 1px solid @red-3;
}
.react-flow__handle {
border-color: @red-3;
}
.lineage-node-handle {
border: 1px solid @red-3;
svg {
color: @red-3;
}
}
&.custom-node-header-active {
border-color: @red-3;
}
}
.custom-node-header-active { .custom-node-header-active {
border-color: @primary-color; border-color: @primary-color;
} }

View File

@ -52,6 +52,7 @@ export interface EdgeDetails {
columns?: ColumnLineage[]; columns?: ColumnLineage[];
description?: string; description?: string;
pipelineEntityType?: EntityType.PIPELINE | EntityType.STORED_PROCEDURE; pipelineEntityType?: EntityType.PIPELINE | EntityType.STORED_PROCEDURE;
doc_id?: string;
} }
export type LineageSourceType = Omit<SourceType, 'service'> & { export type LineageSourceType = Omit<SourceType, 'service'> & {

View File

@ -15,7 +15,10 @@ import React, { useEffect } from 'react';
import { Edge } from 'reactflow'; import { Edge } from 'reactflow';
import { EdgeTypeEnum } from '../../components/Entity/EntityLineage/EntityLineage.interface'; import { EdgeTypeEnum } from '../../components/Entity/EntityLineage/EntityLineage.interface';
import { EntityType } from '../../enums/entity.enum'; import { EntityType } from '../../enums/entity.enum';
import { getLineageDataByFQN } from '../../rest/lineageAPI'; import {
getDataQualityLineage,
getLineageDataByFQN,
} from '../../rest/lineageAPI';
import LineageProvider, { useLineageProvider } from './LineageProvider'; import LineageProvider, { useLineageProvider } from './LineageProvider';
const mockLocation = { const mockLocation = {
@ -128,9 +131,16 @@ jest.mock(
}); });
} }
); );
let mockIsAlertSupported = false;
jest.mock('../../utils/TableClassBase', () => ({
getAlertEnableStatus: jest
.fn()
.mockImplementation(() => mockIsAlertSupported),
}));
jest.mock('../../rest/lineageAPI', () => ({ jest.mock('../../rest/lineageAPI', () => ({
getLineageDataByFQN: jest.fn(), getLineageDataByFQN: jest.fn(),
getDataQualityLineage: jest.fn(),
})); }));
describe('LineageProvider', () => { describe('LineageProvider', () => {
@ -148,6 +158,38 @@ describe('LineageProvider', () => {
}); });
expect(getLineageDataByFQN).toHaveBeenCalled(); expect(getLineageDataByFQN).toHaveBeenCalled();
expect(getDataQualityLineage).not.toHaveBeenCalled();
});
it('getDataQualityLineage should be called if alert is supported', async () => {
mockIsAlertSupported = true;
(getLineageDataByFQN as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({
nodes: [],
edges: [],
})
);
await act(async () => {
render(
<LineageProvider>
<DummyChildrenComponent />
</LineageProvider>
);
});
expect(getLineageDataByFQN).toHaveBeenCalledWith(
'table1',
'table',
{ downstreamDepth: 1, nodesPerLayer: 50, upstreamDepth: 1 },
''
);
expect(getDataQualityLineage).toHaveBeenCalledWith(
'table1',
{ downstreamDepth: 1, nodesPerLayer: 50, upstreamDepth: 1 },
''
);
mockIsAlertSupported = false;
}); });
it('should call loadChildNodesHandler', async () => { it('should call loadChildNodesHandler', async () => {

View File

@ -14,8 +14,9 @@ import Icon from '@ant-design/icons/lib/components/Icon';
import { Button, Modal } from 'antd'; import { Button, Modal } from 'antd';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import classNames from 'classnames'; import classNames from 'classnames';
import { isEqual, isUndefined, uniqueId, uniqWith } from 'lodash'; import { isEqual, isUndefined, uniq, uniqueId, uniqWith } from 'lodash';
import { LoadingState } from 'Models'; import { LoadingState } from 'Models';
import QueryString from 'qs';
import React, { import React, {
createContext, createContext,
DragEvent, DragEvent,
@ -76,8 +77,13 @@ import {
LineageDetails, LineageDetails,
} from '../../generated/type/entityLineage'; } from '../../generated/type/entityLineage';
import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useApplicationStore } from '../../hooks/useApplicationStore';
import useCustomLocation from '../../hooks/useCustomLocation/useCustomLocation';
import { useFqn } from '../../hooks/useFqn'; import { useFqn } from '../../hooks/useFqn';
import { getLineageDataByFQN, updateLineageEdge } from '../../rest/lineageAPI'; import {
getDataQualityLineage,
getLineageDataByFQN,
updateLineageEdge,
} from '../../rest/lineageAPI';
import { import {
addLineageHandler, addLineageHandler,
centerNodePosition, centerNodePosition,
@ -104,6 +110,7 @@ import {
removeLineageHandler, removeLineageHandler,
} from '../../utils/EntityLineageUtils'; } from '../../utils/EntityLineageUtils';
import { getEntityReferenceFromEntity } from '../../utils/EntityUtils'; import { getEntityReferenceFromEntity } from '../../utils/EntityUtils';
import tableClassBase from '../../utils/TableClassBase';
import { showErrorToast } from '../../utils/ToastUtils'; import { showErrorToast } from '../../utils/ToastUtils';
import { useTourProvider } from '../TourProvider/TourProvider'; import { useTourProvider } from '../TourProvider/TourProvider';
import { import {
@ -117,6 +124,7 @@ export const LineageContext = createContext({} as LineageContextType);
const LineageProvider = ({ children }: LineageProviderProps) => { const LineageProvider = ({ children }: LineageProviderProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { fqn: decodedFqn } = useFqn(); const { fqn: decodedFqn } = useFqn();
const location = useCustomLocation();
const { isTourOpen, isTourPage } = useTourProvider(); const { isTourOpen, isTourPage } = useTourProvider();
const { appPreferences } = useApplicationStore(); const { appPreferences } = useApplicationStore();
const defaultLineageConfig = appPreferences?.lineageConfig as LineageSettings; const defaultLineageConfig = appPreferences?.lineageConfig as LineageSettings;
@ -180,6 +188,15 @@ const LineageProvider = ({ children }: LineageProviderProps) => {
const [paginationData, setPaginationData] = useState({}); const [paginationData, setPaginationData] = useState({});
const { showModal } = useEntityExportModalProvider(); const { showModal } = useEntityExportModalProvider();
const lineageLayer = useMemo(() => {
const param = location.search;
const searchData = QueryString.parse(
param.startsWith('?') ? param.substring(1) : param
);
return searchData.layers as LineageLayer[] | undefined;
}, [location.search]);
const initLineageChildMaps = useCallback( const initLineageChildMaps = useCallback(
( (
lineageData: EntityLineageResponse, lineageData: EntityLineageResponse,
@ -220,25 +237,63 @@ const LineageProvider = ({ children }: LineageProviderProps) => {
config, config,
queryFilter queryFilter
); );
const dqLineageResp =
entityType === EntityType.TABLE &&
tableClassBase.getAlertEnableStatus()
? await getDataQualityLineage(fqn, config, queryFilter)
: { nodes: [], edges: [] };
if (res) { if (res) {
const { nodes = [], entity } = res; const { nodes = [], entity, edges } = res;
const allNodes = uniqWith( const allNodes = uniqWith(
[...nodes, entity].filter(Boolean), [...nodes, entity].filter(Boolean),
isEqual isEqual
); ).map((node) => {
return {
...node,
isDqTestFailure:
dqLineageResp.nodes?.some((dqNode) => dqNode.id === node.id) ??
false,
};
});
const updatedEntity = {
...entity,
isDqTestFailure:
dqLineageResp.nodes?.some((dqNode) => dqNode.id === entity.id) ??
false,
};
const updatedEdges = edges?.map((edge) => {
return {
...edge,
isDqTestFailure:
dqLineageResp.edges?.some(
(dqEdge) => dqEdge?.doc_id === edge?.doc_id
) ?? false,
};
});
if ( if (
entityType !== EntityType.PIPELINE && entityType !== EntityType.PIPELINE &&
entityType !== EntityType.STORED_PROCEDURE entityType !== EntityType.STORED_PROCEDURE
) { ) {
const { map: childMapObj } = getChildMap( const { map: childMapObj } = getChildMap(
{ ...res, nodes: allNodes }, {
...res,
nodes: allNodes,
edges: updatedEdges,
entity: updatedEntity,
},
decodedFqn decodedFqn
); );
setChildMap(childMapObj); setChildMap(childMapObj);
const { nodes: newNodes, edges: newEdges } = getPaginatedChildMap( const { nodes: newNodes, edges: newEdges } = getPaginatedChildMap(
{ {
...res, ...res,
entity: updatedEntity,
edges: updatedEdges,
nodes: allNodes, nodes: allNodes,
}, },
childMapObj, childMapObj,
@ -248,12 +303,14 @@ const LineageProvider = ({ children }: LineageProviderProps) => {
setEntityLineage({ setEntityLineage({
...res, ...res,
entity: updatedEntity,
nodes: newNodes, nodes: newNodes,
edges: [...(res.edges ?? []), ...newEdges], edges: [...(updatedEdges ?? []), ...newEdges],
}); });
} else { } else {
setEntityLineage({ setEntityLineage({
...res, ...res,
entity: updatedEntity,
nodes: allNodes, nodes: allNodes,
}); });
} }
@ -1308,6 +1365,12 @@ const LineageProvider = ({ children }: LineageProviderProps) => {
} }
}, [isTourOpen, isTourPage]); }, [isTourOpen, isTourPage]);
useEffect(() => {
if (lineageLayer) {
setActiveLayer((pre) => uniq([...lineageLayer, ...pre]));
}
}, [lineageLayer]);
return ( return (
<LineageContext.Provider value={activityFeedContextValues}> <LineageContext.Provider value={activityFeedContextValues}>
<div <div

View File

@ -48,7 +48,8 @@ export const getLineageDataByFQN = async (
export const getDataQualityLineage = async ( export const getDataQualityLineage = async (
fqn: string, fqn: string,
config?: Partial<LineageConfig> config?: Partial<LineageConfig>,
queryFilter?: string
) => { ) => {
const { upstreamDepth = 1 } = config ?? {}; const { upstreamDepth = 1 } = config ?? {};
const response = await APIClient.get<EntityLineageResponse>( const response = await APIClient.get<EntityLineageResponse>(
@ -58,6 +59,7 @@ export const getDataQualityLineage = async (
fqn, fqn,
upstreamDepth, upstreamDepth,
includeDeleted: false, includeDeleted: false,
query_filter: queryFilter,
}, },
} }
); );