ISSUE #21216: added include field to incident list (#21221)

* fix: added include field to incident list

* added ui changes for allowing deleted entries in entity page and fix the count on incident page

* fix: error retrieving soft deleted test cases

* fix: ran java linting

* Update openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.component.tsx

Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com>

* fix testcase playwright

* fix playwright test

---------

Co-authored-by: Ashish Gupta <ashish@getcollate.io>
Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com>
(cherry picked from commit 0b483ecb865341425abf47132185bdbde6051bc7)
This commit is contained in:
Teddy 2025-05-18 21:35:13 +02:00 committed by Ashish Gupta
parent 5b83a2949f
commit 3998982d54
11 changed files with 170 additions and 17 deletions

View File

@ -374,10 +374,11 @@ public class TestCaseResolutionStatusRepository
public static String addOriginEntityFQNJoin(ListFilter filter, String condition) {
// if originEntityFQN is present, we need to join with test_case table
if (filter.getQueryParam("originEntityFQN") != null) {
if ((filter.getQueryParam("originEntityFQN") != null)
|| (filter.getQueryParam("include") != null)) {
condition =
"""
INNER JOIN (SELECT entityFQN AS testCaseEntityFQN,fqnHash AS testCaseHash FROM test_case) tc \
INNER JOIN (SELECT entityFQN AS testCaseEntityFQN,fqnHash AS testCaseHash, deleted FROM test_case) tc \
ON entityFQNHash = testCaseHash
"""
+ condition;

View File

@ -37,6 +37,7 @@ import lombok.extern.slf4j.Slf4j;
import org.openmetadata.schema.api.tests.CreateTestCaseResolutionStatus;
import org.openmetadata.schema.tests.type.TestCaseResolutionStatus;
import org.openmetadata.schema.tests.type.TestCaseResolutionStatusTypes;
import org.openmetadata.schema.type.Include;
import org.openmetadata.schema.type.MetadataOperation;
import org.openmetadata.service.Entity;
import org.openmetadata.service.jdbi3.ListFilter;
@ -137,6 +138,12 @@ public class TestCaseResolutionStatusResource
schema = @Schema(type = "String"))
@QueryParam("assignee")
String assignee,
@Parameter(
description = "Include all, deleted, or non-deleted entities.",
schema = @Schema(implementation = Include.class))
@QueryParam("include")
@DefaultValue("non-deleted")
Include include,
@Parameter(description = "Test case fully qualified name", schema = @Schema(type = "String"))
@QueryParam("testCaseFQN")
String testCaseFQN,
@ -159,7 +166,7 @@ public class TestCaseResolutionStatusResource
}
authorizer.authorizeRequests(securityContext, requests, AuthorizationLogic.ANY);
ListFilter filter = new ListFilter(null);
ListFilter filter = new ListFilter(include);
filter.addQueryParam("testCaseResolutionStatusType", testCaseResolutionStatusType);
filter.addQueryParam("assignee", assignee);
filter.addQueryParam("entityFQNHash", FullyQualifiedName.buildHash(testCaseFQN));

View File

@ -185,7 +185,7 @@ public class PIIMasker {
Entity.TABLE,
testCaseLink.getEntityFQN(),
"owners,tags,columns",
Include.NON_DELETED);
Include.ALL);
entityFQNToTable.put(testCaseLink.getEntityFQN(), table);
}

View File

@ -1525,8 +1525,11 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
// 1. soft delete - should not delete the test case resolution status
// 2. hard delete - should delete the test case resolution status
deleteEntity(testCaseEntity1.getId(), true, false, ADMIN_AUTH_HEADERS);
queryParams.clear();
queryParams.put("include", "all");
storedTestCaseResolutions =
getTestCaseFailureStatus(startTs, endTs, null, TestCaseResolutionStatusTypes.Ack);
getTestCaseFailureStatus(
startTs, endTs, null, TestCaseResolutionStatusTypes.Ack, queryParams);
assertEquals(2, storedTestCaseResolutions.getData().size());
assertTrue(
storedTestCaseResolutions.getData().stream()
@ -1570,6 +1573,40 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
paginateTestCaseFailureStatus(maxEntities, allEntities, startTs, endTs);
}
@Test
void test_listTestCaseFailureStatusDeletedTestCase(TestInfo test)
throws IOException, ParseException {
List<TestCase> testCases = new ArrayList<>();
Long startTs = System.currentTimeMillis() - 1000;
for (int i = 0; i < 2; i++) {
// We'll create random test cases
TestCase testCaseEntity =
createEntity(createRequest(getEntityName(test) + i), ADMIN_AUTH_HEADERS);
testCases.add(testCaseEntity);
// Adding failed test case, which will create a NEW incident
postTestCaseResult(
testCaseEntity.getFullyQualifiedName(),
new CreateTestCaseResult()
.withResult("result")
.withTestCaseStatus(TestCaseStatus.Failed)
.withTimestamp(TestUtils.dateToTimestamp("2024-01-01")),
ADMIN_AUTH_HEADERS);
}
Long endTs = System.currentTimeMillis() + 1000;
ResultList<TestCaseResolutionStatus> entities =
getTestCaseFailureStatus(1000, null, false, startTs, endTs, null);
assertTrue(
entities.getData().stream()
.anyMatch(
tcrs -> tcrs.getTestCaseReference().getId().equals(testCases.get(0).getId())));
deleteEntityByName(testCases.get(0).getFullyQualifiedName(), true, false, ADMIN_AUTH_HEADERS);
entities = getTestCaseFailureStatus(1000, null, false, startTs, endTs, null);
assertTrue(
entities.getData().stream()
.noneMatch(
tcrs -> tcrs.getTestCaseReference().getId().equals(testCases.get(0).getId())));
}
@Test
void patch_TestCaseResultFailure(TestInfo test) throws HttpResponseException {
TestCase testCaseEntity = createEntity(createRequest(getEntityName(test)), ADMIN_AUTH_HEADERS);

View File

@ -254,7 +254,7 @@ test.describe('Incident Manager', PLAYWRIGHT_INGESTION_TAG_OBJ, () => {
);
const incidentDetailsRes = page.waitForResponse(
'/api/v1/dataQuality/testCases/testCaseIncidentStatus?latest=true&startTs=*&endTs=*&limit=*'
'/api/v1/dataQuality/testCases/testCaseIncidentStatus?*'
);
await sidebarClick(page, SidebarItem.INCIDENT_MANAGER);
await incidentDetailsRes;
@ -456,7 +456,7 @@ test.describe('Incident Manager', PLAYWRIGHT_INGESTION_TAG_OBJ, () => {
};
const testCase1 = table1.testCasesResponseData[0]?.['name'];
const incidentDetailsRes = page.waitForResponse(
'/api/v1/dataQuality/testCases/testCaseIncidentStatus?latest=true&startTs=*&endTs=*&limit=*'
'/api/v1/dataQuality/testCases/testCaseIncidentStatus?*'
);
await sidebarClick(page, SidebarItem.INCIDENT_MANAGER);
await incidentDetailsRes;
@ -485,7 +485,7 @@ test.describe('Incident Manager', PLAYWRIGHT_INGESTION_TAG_OBJ, () => {
).not.toBeVisible();
const nonAssigneeFilterRes = page.waitForResponse(
'/api/v1/dataQuality/testCases/testCaseIncidentStatus?latest=true&startTs=*&endTs=*&limit=*'
'/api/v1/dataQuality/testCases/testCaseIncidentStatus?*'
);
await page
.getByTestId('select-assignee')
@ -508,7 +508,7 @@ test.describe('Incident Manager', PLAYWRIGHT_INGESTION_TAG_OBJ, () => {
).not.toBeVisible();
const nonStatusFilterRes = page.waitForResponse(
'/api/v1/dataQuality/testCases/testCaseIncidentStatus?latest=true&startTs=*&endTs=*&limit=*'
'/api/v1/dataQuality/testCases/testCaseIncidentStatus?*'
);
await page.getByTestId('status-select').getByLabel('close-circle').click();
await nonStatusFilterRes;
@ -534,7 +534,7 @@ test.describe('Incident Manager', PLAYWRIGHT_INGESTION_TAG_OBJ, () => {
).toBeVisible();
const nonTestCaseFilterRes = page.waitForResponse(
'/api/v1/dataQuality/testCases/testCaseIncidentStatus?latest=true&startTs=*&endTs=*&limit=*'
'/api/v1/dataQuality/testCases/testCaseIncidentStatus?*'
);
await page
.getByTestId('test-case-select')
@ -544,7 +544,7 @@ test.describe('Incident Manager', PLAYWRIGHT_INGESTION_TAG_OBJ, () => {
await page.click('[data-testid="date-picker-menu"]');
const timeSeriesFilterRes = page.waitForResponse(
'/api/v1/dataQuality/testCases/testCaseIncidentStatus?latest=true&startTs=*&endTs=*&limit=*'
'/api/v1/dataQuality/testCases/testCaseIncidentStatus?*'
);
await page.getByRole('menuitem', { name: 'Yesterday' }).click();
await timeSeriesFilterRes;

View File

@ -73,19 +73,23 @@ const mockPermissions = {
} as OperationPermission;
describe('TableProfilerProvider', () => {
beforeEach(() => {
it('renders children without crashing', async () => {
render(
<TableProfilerProvider permissions={mockPermissions} table={MOCK_TABLE}>
<div>Test Children</div>
</TableProfilerProvider>
);
});
it('renders children without crashing', async () => {
expect(await screen.findByText('Test Children')).toBeInTheDocument();
});
it('test cases should be fetch on data quality tab', async () => {
render(
<TableProfilerProvider permissions={mockPermissions} table={MOCK_TABLE}>
<div>Test Children</div>
</TableProfilerProvider>
);
const mockGetListTestCase = getListTestCaseBySearch as jest.Mock;
expect(mockGetListTestCase).toHaveBeenCalledTimes(1);
@ -94,6 +98,28 @@ describe('TableProfilerProvider', () => {
fields: ['testCaseResult', 'incidentId'],
includeAllTests: true,
limit: 10,
include: 'non-deleted',
});
});
it('test cases should be fetch on data quality tab with deleted', async () => {
render(
<TableProfilerProvider
permissions={mockPermissions}
table={{ ...MOCK_TABLE, deleted: true }}>
<div>Test Children</div>
</TableProfilerProvider>
);
const mockGetListTestCase = getListTestCaseBySearch as jest.Mock;
expect(mockGetListTestCase).toHaveBeenCalledTimes(1);
expect(mockGetListTestCase).toHaveBeenCalledWith({
entityLink: 'entityLink',
fields: ['testCaseResult', 'incidentId'],
includeAllTests: true,
limit: 10,
include: 'deleted',
});
});
});

View File

@ -35,6 +35,7 @@ import { TabSpecificField } from '../../../../enums/entity.enum';
import { Table } from '../../../../generated/entity/data/table';
import { ProfileSampleType } from '../../../../generated/metadataIngestion/databaseServiceProfilerPipeline';
import { TestCase } from '../../../../generated/tests/testCase';
import { Include } from '../../../../generated/type/include';
import { usePaging } from '../../../../hooks/paging/usePaging';
import useCustomLocation from '../../../../hooks/useCustomLocation/useCustomLocation';
import { useFqn } from '../../../../hooks/useFqn';
@ -222,6 +223,7 @@ export const TableProfilerProvider = ({
entityLink: generateEntityLink(datasetFQN ?? ''),
includeAllTests: true,
limit: testCasePaging.pageSize,
include: isTableDeleted ? Include.Deleted : Include.NonDeleted,
});
setAllTestCases(data);

View File

@ -35,6 +35,7 @@ import {
TestCaseResolutionStatus,
TestCaseResolutionStatusTypes,
} from '../../generated/tests/testCaseResolutionStatus';
import { Include } from '../../generated/type/include';
import { usePaging } from '../../hooks/paging/usePaging';
import useCustomLocation from '../../hooks/useCustomLocation/useCustomLocation';
import {
@ -154,6 +155,7 @@ const IncidentManager = ({
const { data, paging } = await getListTestCaseIncidentStatus({
limit: pageSize,
latest: true,
include: tableDetails?.deleted ? Include.Deleted : Include.NonDeleted,
originEntityFQN: tableDetails?.fullyQualifiedName,
...params,
});
@ -463,7 +465,7 @@ const IncidentManager = ({
return (
<TestCaseIncidentManagerStatus
data={record}
hasPermission={hasPermission?.EditAll}
hasPermission={hasPermission?.EditAll && !tableDetails?.deleted}
onSubmit={handleStatusSubmit}
/>
);
@ -487,7 +489,7 @@ const IncidentManager = ({
return (
<Severity
hasPermission={hasPermission?.EditAll}
hasPermission={hasPermission?.EditAll && !tableDetails?.deleted}
severity={value}
onSubmit={(severity) => handleSeveritySubmit(severity, record)}
/>
@ -507,7 +509,12 @@ const IncidentManager = ({
),
},
],
[testCaseListData.data, testCasePermissions, isPermissionLoading]
[
tableDetails?.deleted,
testCaseListData.data,
testCasePermissions,
isPermissionLoading,
]
);
if (

View File

@ -13,6 +13,7 @@
import { act, fireEvent, render, screen } from '@testing-library/react';
import QueryString from 'qs';
import React from 'react';
import { Table } from '../../generated/entity/data/table';
import { MOCK_PERMISSIONS } from '../../mocks/Glossary.mock';
import { getListTestCaseIncidentStatus } from '../../rest/incidentManagerAPI';
import IncidentManager from './IncidentManager.component';
@ -153,6 +154,29 @@ describe('IncidentManagerPage', () => {
latest: true,
limit: 10,
startTs: 1709556624254,
include: 'non-deleted',
});
});
it('Incident should be fetch with deleted', async () => {
const mockGetListTestCaseIncidentStatus =
getListTestCaseIncidentStatus as jest.Mock;
await act(async () => {
render(<IncidentManager tableDetails={{ deleted: true } as Table} />);
});
const timeFilterButton = await screen.findByTestId('time-filter');
await act(async () => {
fireEvent.click(timeFilterButton);
});
expect(mockGetListTestCaseIncidentStatus).toHaveBeenCalledWith({
endTs: 1710161424255,
latest: true,
limit: 10,
startTs: 1709556624254,
include: 'deleted',
});
});

View File

@ -20,6 +20,7 @@ import {
} from '../../generated/tests/testDefinition';
import { ListTestCaseParamsBySearch } from '../../rest/testAPI';
import {
buildDataQualityDashboardFilters,
buildMustEsFilterForOwner,
buildMustEsFilterForTags,
buildTestCaseParams,
@ -738,4 +739,45 @@ describe('DataQualityUtils', () => {
);
});
});
describe('buildDataQualityDashboardFilters', () => {
it('should include deleted:false filter by default', () => {
const result = buildDataQualityDashboardFilters({});
expect(result).toEqual([
{
term: {
deleted: false,
},
},
]);
});
it('should include deleted:false filter along with other filters', () => {
const result = buildDataQualityDashboardFilters({
filters: {
serviceName: 'test-service',
testPlatforms: ['DBT'],
},
});
expect(result).toEqual([
{
term: {
'service.name.keyword': 'test-service',
},
},
{
terms: {
testPlatforms: ['DBT'],
},
},
{
term: {
deleted: false,
},
},
]);
});
});
});

View File

@ -325,6 +325,13 @@ export const buildDataQualityDashboardFilters = (data: {
}
}
// Add the deleted filter to the mustFilter array
mustFilter.push({
term: {
deleted: false,
},
});
return mustFilter;
};