Merge remote-tracking branch 'origin/main' into fix-superset-form-bug

This commit is contained in:
Aniket Katkar 2025-09-26 22:15:52 +05:30
commit 77e16cfb6d
50 changed files with 1359 additions and 299 deletions

View File

@ -10,6 +10,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
RED='\033[0;31m'
cd "$(dirname "${BASH_SOURCE[0]}")" || exit
helpFunction()
@ -116,6 +118,13 @@ until curl -s -f "http://localhost:9200/_cat/indices/openmetadata_team_search_in
done
until curl -s -f --header 'Authorization: Basic YWRtaW46YWRtaW4=' "http://localhost:8080/api/v1/dags/sample_data"; do
IMPORT_ERRORS="$(curl -s --header 'Authorization: Basic YWRtaW46YWRtaW4=' "http://localhost:8080/api/v1/importErrors")"
echo $IMPORT_ERRORS | grep "/airflow_sample_data.py" > /dev/null;
if [[ "$?" == "0" ]]; then
echo -e "${RED}Airflow found an error importing \`sample_data\` DAG"
printf '%s' "$IMPORT_ERRORS" | jq '.["import_errors"] | .[] | select(.filename | endswith("airflow_sample_data.py"))'
exit 1
fi
echo 'Checking if Sample Data DAG is reachable...\n'
sleep 5
done

View File

@ -72,7 +72,9 @@ class DatabricksClient:
"Content-Type": "application/json",
}
self.api_timeout = self.config.connectionTimeout or 120
self._job_table_lineage_executed: bool = False
self.job_table_lineage: dict[str, list[dict[str, str]]] = {}
self._job_column_lineage_executed: bool = False
self.job_column_lineage: dict[
str, dict[Tuple[str, str], list[Tuple[str, str]]]
] = {}
@ -193,7 +195,6 @@ class DatabricksClient:
"""
Method returns List all the created jobs in a Databricks Workspace
"""
self.cache_lineage()
try:
iteration_count = 1
data = {"limit": PAGE_SIZE, "expand_tasks": True, "offset": 0}
@ -266,6 +267,10 @@ class DatabricksClient:
Method returns table lineage for a job by the specified job_id
"""
try:
if not self._job_table_lineage_executed:
logger.debug("Executing cache_lineage...")
self.cache_lineage()
return self.job_table_lineage.get(str(job_id))
except Exception as exc:
logger.debug(
@ -281,6 +286,10 @@ class DatabricksClient:
Method returns column lineage for a job by the specified job_id and table key
"""
try:
if not self._job_column_lineage_executed:
logger.debug("Job column lineage not found. Executing cache_lineage...")
self.cache_lineage()
return self.job_column_lineage.get(str(job_id), {}).get(TableKey)
except Exception as exc:
logger.debug(
@ -325,6 +334,7 @@ class DatabricksClient:
f"Error parsing row: {row} due to {traceback.format_exc()}"
)
continue
self._job_table_lineage_executed = True
# Not every job has column lineage, so we need to check if the job exists in the column_lineage table
# we will cache the column lineage for jobs that have column lineage
@ -355,3 +365,5 @@ class DatabricksClient:
f"Error parsing row: {row} due to {traceback.format_exc()}"
)
continue
self._job_column_lineage_executed = True
logger.debug("Table and column lineage caching completed.")

View File

@ -96,6 +96,7 @@ DATABRICKS_GET_TABLE_LINEAGE_FOR_JOB = textwrap.dedent(
target_table_full_name as target_table_full_name
FROM system.access.table_lineage
WHERE entity_type ILIKE 'job'
AND event_time >= current_date() - INTERVAL 90 DAYS
"""
)
@ -110,5 +111,6 @@ DATABRICKS_GET_COLUMN_LINEAGE_FOR_JOB = textwrap.dedent(
target_column_name as target_column_name
FROM system.access.column_lineage
WHERE entity_type ILIKE 'job'
AND event_time >= current_date() - INTERVAL 90 DAYS
"""
)

View File

@ -130,9 +130,11 @@ class DatabrickspipelineSource(PipelineServiceSource):
displayName=pipeline_details.settings.name,
description=Markdown(description) if description else None,
tasks=self.get_tasks(pipeline_details),
scheduleInterval=str(pipeline_details.settings.schedule.cron)
if pipeline_details.settings.schedule
else None,
scheduleInterval=(
str(pipeline_details.settings.schedule.cron)
if pipeline_details.settings.schedule
else None
),
service=FullyQualifiedEntityName(self.context.get().pipeline_service),
)
yield Either(right=pipeline_request)
@ -176,12 +178,14 @@ class DatabrickspipelineSource(PipelineServiceSource):
Task(
name=str(task.name),
taskType=pipeline_details.settings.task_type,
sourceUrl=SourceUrl(run.run_page_url)
if run.run_page_url
else None,
description=Markdown(task.description)
if task.description
else None,
sourceUrl=(
SourceUrl(run.run_page_url)
if run.run_page_url
else None
),
description=(
Markdown(task.description) if task.description else None
),
downstreamTasks=[
depend_task.name
for depend_task in task.depends_on or []
@ -314,6 +318,9 @@ class DatabrickspipelineSource(PipelineServiceSource):
table_lineage_list = self.client.get_table_lineage(
job_id=pipeline_details.job_id
)
logger.debug(
f"Processing pipeline lineage for job {pipeline_details.job_id}"
)
if table_lineage_list:
for table_lineage in table_lineage_list:
source_table_full_name = table_lineage.get("source_table_full_name")

View File

@ -15,10 +15,11 @@ import { expect, test } from '@playwright/test';
import { RDG_ACTIVE_CELL_SELECTOR } from '../../constant/bulkImportExport';
import { SERVICE_TYPE } from '../../constant/service';
import { GlobalSettingOptions } from '../../constant/settings';
import { EntityDataClass } from '../../support/entity/EntityDataClass';
import { Domain } from '../../support/domain/Domain';
import { TableClass } from '../../support/entity/TableClass';
import { Glossary } from '../../support/glossary/Glossary';
import { GlossaryTerm } from '../../support/glossary/GlossaryTerm';
import { UserClass } from '../../support/user/UserClass';
import {
createNewPage,
descriptionBoxReadOnly,
@ -49,9 +50,16 @@ test.use({
storageState: 'playwright/.auth/admin.json',
});
const user1 = new UserClass();
const user2 = new UserClass();
const glossary = new Glossary();
const glossaryTerm = new GlossaryTerm(glossary);
const domain1 = new Domain();
const domain2 = new Domain();
const glossaryDetails = {
name: EntityDataClass.glossaryTerm1.data.name,
parent: EntityDataClass.glossary1.data.name,
name: glossaryTerm.data.name,
parent: glossary.data.name,
};
const databaseSchemaDetails1 = {
@ -70,19 +78,16 @@ const columnDetails1 = {
};
test.describe('Bulk Edit Entity', () => {
test.beforeAll('setup pre-test', async ({ browser }, testInfo) => {
test.beforeAll('setup pre-test', async ({ browser }) => {
const { apiContext, afterAction } = await createNewPage(browser);
testInfo.setTimeout(90000);
await EntityDataClass.preRequisitesForTests(apiContext);
await afterAction();
});
await user1.create(apiContext);
await user2.create(apiContext);
await glossary.create(apiContext);
await glossaryTerm.create(apiContext);
await domain1.create(apiContext);
await domain2.create(apiContext);
test.afterAll('Cleanup', async ({ browser }, testInfo) => {
const { apiContext, afterAction } = await createNewPage(browser);
testInfo.setTimeout(90000);
await EntityDataClass.postRequisitesForTests(apiContext);
await afterAction();
});
@ -109,7 +114,7 @@ test.describe('Bulk Edit Entity', () => {
await test.step('Perform bulk edit action', async () => {
const databaseDetails = {
...createDatabaseRowDetails(),
domains: EntityDataClass.domain1.responseData,
domains: domain1.responseData,
glossary: glossaryDetails,
};
@ -145,8 +150,8 @@ test.describe('Bulk Edit Entity', () => {
...databaseDetails,
name: table.database.name,
owners: [
EntityDataClass.user1.responseData?.['displayName'],
EntityDataClass.user2.responseData?.['displayName'],
user1.responseData?.['displayName'],
user2.responseData?.['displayName'],
],
retentionPeriod: undefined,
sourceUrl: undefined,
@ -191,10 +196,10 @@ test.describe('Bulk Edit Entity', () => {
// Verify Owners
await expect(
page.getByTestId(EntityDataClass.user1.responseData?.['displayName'])
page.getByTestId(user1.responseData?.['displayName'])
).toBeVisible();
await expect(
page.getByTestId(EntityDataClass.user2.responseData?.['displayName'])
page.getByTestId(user2.responseData?.['displayName'])
).toBeVisible();
// Verify Tags
@ -212,7 +217,7 @@ test.describe('Bulk Edit Entity', () => {
await expect(
page.getByRole('link', {
name: EntityDataClass.glossaryTerm1.data.displayName,
name: glossaryTerm.data.displayName,
})
).toBeVisible();
});
@ -280,10 +285,10 @@ test.describe('Bulk Edit Entity', () => {
...databaseSchemaDetails1,
name: table.schema.name,
owners: [
EntityDataClass.user1.responseData?.['displayName'],
EntityDataClass.user2.responseData?.['displayName'],
user1.responseData?.['displayName'],
user2.responseData?.['displayName'],
],
domains: EntityDataClass.domain1.responseData,
domains: domain1.responseData,
},
page,
customPropertyRecord
@ -327,11 +332,11 @@ test.describe('Bulk Edit Entity', () => {
// Verify Owners
await expect(
page.getByTestId(EntityDataClass.user1.responseData?.['displayName'])
page.getByTestId(user1.responseData?.['displayName'])
).toBeVisible();
await expect(
page.getByTestId(EntityDataClass.user2.responseData?.['displayName'])
page.getByTestId(user2.responseData?.['displayName'])
).toBeVisible();
await page.getByTestId('column-display-name').click();
@ -354,7 +359,7 @@ test.describe('Bulk Edit Entity', () => {
await expect(
page.getByRole('link', {
name: EntityDataClass.glossaryTerm1.data.displayName,
name: glossaryTerm.data.displayName,
})
).toBeVisible();
});
@ -420,10 +425,10 @@ test.describe('Bulk Edit Entity', () => {
...tableDetails1,
name: table.entity.name,
owners: [
EntityDataClass.user1.responseData?.['displayName'],
EntityDataClass.user2.responseData?.['displayName'],
user1.responseData?.['displayName'],
user2.responseData?.['displayName'],
],
domains: EntityDataClass.domain1.responseData,
domains: domain1.responseData,
},
page,
customPropertyRecord
@ -464,16 +469,16 @@ test.describe('Bulk Edit Entity', () => {
// Verify Domain
await expect(page.getByTestId('domain-link')).toContainText(
EntityDataClass.domain1.responseData.displayName
domain1.responseData.displayName
);
// Verify Owners
await expect(
page.getByTestId(EntityDataClass.user1.responseData?.['displayName'])
page.getByTestId(user1.responseData?.['displayName'])
).toBeVisible();
await expect(
page.getByTestId(EntityDataClass.user2.responseData?.['displayName'])
page.getByTestId(user2.responseData?.['displayName'])
).toBeVisible();
// Verify Tags
@ -491,7 +496,7 @@ test.describe('Bulk Edit Entity', () => {
await expect(
page.getByRole('link', {
name: EntityDataClass.glossaryTerm1.data.displayName,
name: glossaryTerm.data.displayName,
})
).toBeVisible();
});
@ -583,7 +588,7 @@ test.describe('Bulk Edit Entity', () => {
await expect(
page.getByRole('link', {
name: EntityDataClass.glossaryTerm1.data.displayName,
name: glossaryTerm.data.displayName,
})
).toBeVisible();
});
@ -626,8 +631,8 @@ test.describe('Bulk Edit Entity', () => {
{
...additionalGlossaryTerm,
name: glossaryTerm.data.name,
owners: [EntityDataClass.user1.responseData?.['displayName']],
reviewers: [EntityDataClass.user2.responseData?.['displayName']],
owners: [user1.responseData?.['displayName']],
reviewers: [user2.responseData?.['displayName']],
relatedTerm: {
parent: glossary.data.name,
name: glossaryTerm.data.name,
@ -689,12 +694,12 @@ test.describe('Bulk Edit Entity', () => {
// Verify Owners
await expect(
page.getByTestId(EntityDataClass.user1.responseData?.['displayName'])
page.getByTestId(user1.responseData?.['displayName'])
).toBeVisible();
// Verify Reviewers
await expect(
page.getByTestId(EntityDataClass.user2.responseData?.['displayName'])
page.getByTestId(user2.responseData?.['displayName'])
).toBeVisible();
});

View File

@ -14,11 +14,14 @@ import { expect, test } from '@playwright/test';
import { RDG_ACTIVE_CELL_SELECTOR } from '../../constant/bulkImportExport';
import { GlobalSettingOptions } from '../../constant/settings';
import { Domain } from '../../support/domain/Domain';
import { DatabaseClass } from '../../support/entity/DatabaseClass';
import { DatabaseSchemaClass } from '../../support/entity/DatabaseSchemaClass';
import { EntityDataClass } from '../../support/entity/EntityDataClass';
import { DatabaseServiceClass } from '../../support/entity/service/DatabaseServiceClass';
import { TableClass } from '../../support/entity/TableClass';
import { Glossary } from '../../support/glossary/Glossary';
import { GlossaryTerm } from '../../support/glossary/GlossaryTerm';
import { UserClass } from '../../support/user/UserClass';
import {
createNewPage,
getApiContext,
@ -53,9 +56,16 @@ test.use({
},
});
const user1 = new UserClass();
const user2 = new UserClass();
const glossary = new Glossary();
const glossaryTerm = new GlossaryTerm(glossary);
const domain1 = new Domain();
const domain2 = new Domain();
const glossaryDetails = {
name: EntityDataClass.glossaryTerm1.data.name,
parent: EntityDataClass.glossary1.data.name,
name: glossaryTerm.data.name,
parent: glossary.data.name,
};
const databaseDetails1 = {
@ -104,19 +114,15 @@ const storedProcedureDetails = {
};
test.describe('Bulk Import Export', () => {
test.beforeAll('setup pre-test', async ({ browser }, testInfo) => {
test.beforeAll('setup pre-test', async ({ browser }) => {
const { apiContext, afterAction } = await createNewPage(browser);
testInfo.setTimeout(90000);
await EntityDataClass.preRequisitesForTests(apiContext);
await afterAction();
});
test.afterAll('Cleanup', async ({ browser }, testInfo) => {
const { apiContext, afterAction } = await createNewPage(browser);
testInfo.setTimeout(90000);
await EntityDataClass.postRequisitesForTests(apiContext);
await user1.create(apiContext);
await user2.create(apiContext);
await glossary.create(apiContext);
await glossaryTerm.create(apiContext);
await domain1.create(apiContext);
await domain2.create(apiContext);
await afterAction();
});
@ -176,10 +182,10 @@ test.describe('Bulk Import Export', () => {
{
...databaseDetails1,
owners: [
EntityDataClass.user1.responseData?.['displayName'],
EntityDataClass.user2.responseData?.['displayName'],
user1.responseData?.['displayName'],
user2.responseData?.['displayName'],
],
domains: EntityDataClass.domain1.responseData,
domains: domain1.responseData,
},
page,
customPropertyRecord
@ -206,10 +212,10 @@ test.describe('Bulk Import Export', () => {
{
...databaseSchemaDetails1,
owners: [
EntityDataClass.user1.responseData?.['displayName'],
EntityDataClass.user2.responseData?.['displayName'],
user1.responseData?.['displayName'],
user2.responseData?.['displayName'],
],
domains: EntityDataClass.domain1.responseData,
domains: domain1.responseData,
},
page
);
@ -236,10 +242,10 @@ test.describe('Bulk Import Export', () => {
{
...tableDetails1,
owners: [
EntityDataClass.user1.responseData?.['displayName'],
EntityDataClass.user2.responseData?.['displayName'],
user1.responseData?.['displayName'],
user2.responseData?.['displayName'],
],
domains: EntityDataClass.domain1.responseData,
domains: domain1.responseData,
},
page
);
@ -284,10 +290,10 @@ test.describe('Bulk Import Export', () => {
{
...storedProcedureDetails,
owners: [
EntityDataClass.user1.responseData?.['displayName'],
EntityDataClass.user2.responseData?.['displayName'],
user1.responseData?.['displayName'],
user2.responseData?.['displayName'],
],
domains: EntityDataClass.domain2.responseData,
domains: domain2.responseData,
},
page
);
@ -317,10 +323,10 @@ test.describe('Bulk Import Export', () => {
{
...databaseDetails2,
owners: [
EntityDataClass.user1.responseData?.['displayName'],
EntityDataClass.user2.responseData?.['displayName'],
user1.responseData?.['displayName'],
user2.responseData?.['displayName'],
],
domains: EntityDataClass.domain2.responseData,
domains: domain2.responseData,
},
page
);
@ -426,10 +432,10 @@ test.describe('Bulk Import Export', () => {
{
...databaseSchemaDetails1,
owners: [
EntityDataClass.user1.responseData?.['displayName'],
EntityDataClass.user2.responseData?.['displayName'],
user1.responseData?.['displayName'],
user2.responseData?.['displayName'],
],
domains: EntityDataClass.domain1.responseData,
domains: domain1.responseData,
},
page,
customPropertyRecord
@ -457,10 +463,10 @@ test.describe('Bulk Import Export', () => {
{
...tableDetails1,
owners: [
EntityDataClass.user1.responseData?.['displayName'],
EntityDataClass.user2.responseData?.['displayName'],
user1.responseData?.['displayName'],
user2.responseData?.['displayName'],
],
domains: EntityDataClass.domain1.responseData,
domains: domain1.responseData,
},
page
);
@ -505,10 +511,10 @@ test.describe('Bulk Import Export', () => {
{
...databaseSchemaDetails2,
owners: [
EntityDataClass.user1.responseData?.['displayName'],
EntityDataClass.user2.responseData?.['displayName'],
user1.responseData?.['displayName'],
user2.responseData?.['displayName'],
],
domains: EntityDataClass.domain1.responseData,
domains: domain1.responseData,
},
page
);
@ -626,10 +632,10 @@ test.describe('Bulk Import Export', () => {
{
...tableDetails1,
owners: [
EntityDataClass.user1.responseData?.['displayName'],
EntityDataClass.user2.responseData?.['displayName'],
user1.responseData?.['displayName'],
user2.responseData?.['displayName'],
],
domains: EntityDataClass.domain1.responseData,
domains: domain1.responseData,
},
page,
customPropertyRecord
@ -676,10 +682,10 @@ test.describe('Bulk Import Export', () => {
{
...tableDetails2,
owners: [
EntityDataClass.user1.responseData?.['displayName'],
EntityDataClass.user2.responseData?.['displayName'],
user1.responseData?.['displayName'],
user2.responseData?.['displayName'],
],
domains: EntityDataClass.domain1.responseData,
domains: domain1.responseData,
},
page,
customPropertyRecord
@ -881,10 +887,10 @@ test.describe('Bulk Import Export', () => {
{
...databaseDetails1,
owners: [
EntityDataClass.user1.responseData?.['displayName'],
EntityDataClass.user2.responseData?.['displayName'],
user1.responseData?.['displayName'],
user2.responseData?.['displayName'],
],
domains: EntityDataClass.domain1.responseData,
domains: domain1.responseData,
},
page,
undefined,

View File

@ -19,7 +19,6 @@ import {
toastNotification,
uuid,
} from '../../utils/common';
import { testConnection } from '../../utils/serviceIngestion';
import { settingClick } from '../../utils/sidebar';
const apiServiceConfig = {
@ -56,7 +55,6 @@ test.describe('API service', () => {
.fill(apiServiceConfig.openAPISchemaURL);
await page.locator('#root\\/token').fill(apiServiceConfig.token);
await testConnection(page);
await page.getByTestId('submit-btn').click();
const autoPilotApplicationRequest = page.waitForRequest(

View File

@ -11,33 +11,58 @@
* limitations under the License.
*/
import test, { expect } from '@playwright/test';
import { SidebarItem } from '../../constant/sidebar';
import { Domain } from '../../support/domain/Domain';
import { TableClass } from '../../support/entity/TableClass';
import { UserClass } from '../../support/user/UserClass';
import { createNewPage, redirectToHomePage } from '../../utils/common';
import {
getEncodedFqn,
waitForAllLoadersToDisappear,
} from '../../utils/entity';
import { getJsonTreeObject } from '../../utils/exploreDiscovery';
import { sidebarClick } from '../../utils/sidebar';
// use the admin user to login
test.use({ storageState: 'playwright/.auth/admin.json' });
const table = new TableClass();
const table1 = new TableClass();
const user = new UserClass();
const domain = new Domain();
test.describe('Explore Assets Discovery', () => {
test.beforeAll(async ({ browser }) => {
const { apiContext, afterAction } = await createNewPage(browser);
await user.create(apiContext);
await domain.create(apiContext);
await table.create(apiContext);
await table1.create(apiContext);
await table.patch({
apiContext,
patchData: [
{
op: 'add',
value: {
type: 'user',
id: user.responseData.id,
},
path: '/owners/0',
},
{
op: 'add',
path: '/domains/0',
value: {
id: domain.responseData.id,
type: 'domain',
name: domain.responseData.name,
displayName: domain.responseData.displayName,
},
},
],
});
await table.delete(apiContext, false);
// await table1.delete(apiContext, false);
await afterAction();
});
test.afterAll(async ({ browser }) => {
const { apiContext, afterAction } = await createNewPage(browser);
await table.delete(apiContext);
await table1.delete(apiContext);
await afterAction();
});
@ -229,4 +254,144 @@ test.describe('Explore Assets Discovery', () => {
page.locator('.ant-popover-inner-content').textContent()
).not.toContain(table1.entityResponseData.name);
});
test('Should not display domain and owner of deleted asset in suggestions when showDeleted is off', async ({
page,
}) => {
await sidebarClick(page, SidebarItem.EXPLORE);
await page.waitForLoadState('networkidle');
await waitForAllLoadersToDisappear(page);
// The user should not be visible in the owners filter when the deleted switch is off
await page.click('[data-testid="search-dropdown-Owners"]');
const searchResOwner = page.waitForResponse(
`/api/v1/search/aggregate?index=dataAsset&field=owners.displayName.keyword*deleted=false*`
);
await page.fill(
'[data-testid="search-input"]',
user.responseData.displayName
);
await searchResOwner;
await waitForAllLoadersToDisappear(page);
await expect(
page
.getByTestId('drop-down-menu')
.getByTestId(user.responseData.displayName)
).not.toBeAttached();
await page.getByTestId('close-btn').click();
// The domain should not be visible in the domains filter when the deleted switch is off
await page.click('[data-testid="search-dropdown-Domains"]');
const searchResDomain = page.waitForResponse(
`/api/v1/search/aggregate?index=dataAsset&field=domains.displayName.keyword*deleted=false*`
);
await page.fill(
'[data-testid="search-input"]',
domain.responseData.displayName
);
await searchResDomain;
await waitForAllLoadersToDisappear(page);
await expect(
page
.getByTestId('drop-down-menu')
.getByTestId(domain.responseData.displayName)
).not.toBeAttached();
await page.getByTestId('close-btn').click();
});
test('Should display domain and owner of deleted asset in suggestions when showDeleted is on', async ({
page,
}) => {
await sidebarClick(page, SidebarItem.EXPLORE);
await page.waitForLoadState('networkidle');
await waitForAllLoadersToDisappear(page);
// Click on the show deleted toggle button
await page.getByTestId('show-deleted').click();
await page.waitForLoadState('networkidle');
await waitForAllLoadersToDisappear(page);
// The user should be visible in the owners filter when the deleted switch is on
const ownerSearchText = user.responseData.displayName.toLowerCase();
await page.click('[data-testid="search-dropdown-Owners"]');
const searchResOwner = page.waitForResponse(
`/api/v1/search/aggregate?index=dataAsset&field=owners.displayName.keyword*deleted=true*`
);
await page.fill('[data-testid="search-input"]', ownerSearchText);
await searchResOwner;
await waitForAllLoadersToDisappear(page);
await expect(
page.getByTestId('drop-down-menu').getByTestId(ownerSearchText)
).toBeAttached();
await page
.getByTestId('drop-down-menu')
.getByTestId(ownerSearchText)
.click();
const fetchWithOwner = page.waitForResponse(
`/api/v1/search/query?*deleted=true*owners.displayName.keyword*${ownerSearchText}*`
);
await page.getByTestId('update-btn').click();
await fetchWithOwner;
await page.waitForLoadState('networkidle');
await waitForAllLoadersToDisappear(page);
// The domain should be visible in the domains filter when the deleted switch is on
const domainSearchText = domain.responseData.displayName.toLowerCase();
await page.click('[data-testid="search-dropdown-Domains"]');
const searchResDomain = page.waitForResponse(
`/api/v1/search/aggregate?index=dataAsset&field=domains.displayName.keyword*deleted=true*`
);
await page.fill('[data-testid="search-input"]', domainSearchText);
await searchResDomain;
await waitForAllLoadersToDisappear(page);
await expect(
page.getByTestId('drop-down-menu').getByTestId(domainSearchText)
).toBeAttached();
await page
.getByTestId('drop-down-menu')
.getByTestId(domainSearchText)
.click();
const fetchWithDomain = page.waitForResponse(
`/api/v1/search/query?*deleted=true*domains.displayName.keyword*${getEncodedFqn(
domainSearchText,
true
)}*`
);
await page.getByTestId('update-btn').click();
await fetchWithDomain;
await page.waitForLoadState('networkidle');
await waitForAllLoadersToDisappear(page);
// Only the table option should be visible for the data assets filter when the deleted switch is on
// with the owner and domain filter applied
await page.click('[data-testid="search-dropdown-Data Assets"]');
await expect(
page.getByTestId('drop-down-menu').getByTestId('table')
).toBeAttached();
});
});

View File

@ -26,7 +26,7 @@ class MlFlowIngestionClass extends ServiceBaseClass {
shouldAddDefaultFilters?: boolean;
}) {
const {
shouldTestConnection = true,
shouldTestConnection = false,
shouldAddIngestion = false,
shouldAddDefaultFilters = false,
} = extraParams ?? {};

View File

@ -104,8 +104,6 @@ class ServiceBaseClass {
await this.fillConnectionDetails(page);
if (this.shouldTestConnection) {
expect(page.getByTestId('next-button')).not.toBeVisible();
await testConnection(page);
}

View File

@ -532,3 +532,44 @@ export const executeWithRetry = async <T>(
}
}
};
export const readElementInListWithScroll = async (
page: Page,
locator: Locator,
hierarchyElementLocator: Locator
) => {
const element = locator;
// Reset scroll position to top before starting pagination
await hierarchyElementLocator.hover();
await page.mouse.wheel(0, -99999);
await page.waitForTimeout(1000);
// Retry mechanism for pagination
let elementCount = await element.count();
let retryCount = 0;
const maxRetries = 10;
while (elementCount === 0 && retryCount < maxRetries) {
await hierarchyElementLocator.hover();
await page.mouse.wheel(0, 1000);
await page.waitForTimeout(500);
// Create fresh locator and check if the article is now visible after this retry
const freshArticle = locator;
const count = await freshArticle.count();
// Check if the article is now visible after this retry
elementCount = count;
// If we found the element, validate it and break out of the loop
if (count > 0) {
await expect(freshArticle).toBeVisible();
return; // Exit the function early since we found and validated the article
}
retryCount++;
}
};

View File

@ -26,6 +26,7 @@ import { TagClass } from '../support/tag/TagClass';
import {
clickOutside,
descriptionBox,
readElementInListWithScroll,
redirectToHomePage,
toastNotification,
uuid,
@ -486,8 +487,24 @@ export const assignCertification = async (
certification: TagClass,
endpoint: string
) => {
const certificationResponse = page.waitForResponse(
'/api/v1/tags?parent=Certification&limit=50'
);
await page.getByTestId('edit-certification').click();
await certificationResponse;
await page.waitForSelector('.certification-card-popover', {
state: 'visible',
});
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
await readElementInListWithScroll(
page,
page.getByTestId(
`radio-btn-${certification.responseData.fullyQualifiedName}`
),
page.locator('[data-testid="certification-cards"] .ant-radio-group')
);
await page
.getByTestId(`radio-btn-${certification.responseData.fullyQualifiedName}`)
.click();
@ -503,6 +520,9 @@ export const assignCertification = async (
export const removeCertification = async (page: Page, endpoint: string) => {
await page.getByTestId('edit-certification').click();
await page.waitForSelector('.certification-card-popover', {
state: 'visible',
});
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
const patchRequest = page.waitForResponse(`/api/v1/${endpoint}/*`);
await page.getByTestId('clear-certification').click();

View File

@ -159,19 +159,12 @@ export const testConnection = async (page: Page) => {
const warningBadge = page.locator('[data-testid="warning-badge"]');
const failBadge = page.locator('[data-testid="fail-badge"]');
await expect(successBadge.or(warningBadge).or(failBadge)).toBeVisible({
await expect(successBadge.or(warningBadge)).toBeVisible({
timeout: 2.5 * 60 * 1000,
});
await expect(page.getByTestId('messag-text')).toContainText(
new RegExp(
'Connection test was successful.|' +
'Test connection partially successful: Some steps had failures, we will only ingest partial metadata. Click here to view details.|' +
'Test connection failed, please validate your connection and permissions for the failed steps.',
'g'
)
/Connection test was successful.|Test connection partially successful: Some steps had failures, we will only ingest partial metadata. Click here to view details./g
);
};

View File

@ -17,6 +17,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as CertificationIcon } from '../../assets/svg/ic-certification.svg';
import { Tag } from '../../generated/entity/classification/tag';
import { Paging } from '../../generated/type/paging';
import { getTags } from '../../rest/tagAPI';
import { getEntityName } from '../../utils/EntityUtils';
import { stringToHTML } from '../../utils/StringsUtils';
@ -35,59 +36,84 @@ const Certification = ({
onClose,
}: CertificationProps) => {
const { t } = useTranslation();
const popoverRef = useRef<any>(null);
const popoverRef = useRef<{ close: () => void } | null>(null);
const [isLoadingCertificationData, setIsLoadingCertificationData] =
useState<boolean>(false);
const [hasContentLoading, setHasContentLoading] = useState<boolean>(false);
const [certifications, setCertifications] = useState<Array<Tag>>([]);
const [selectedCertification, setSelectedCertification] = useState<string>(
currentCertificate ?? ''
);
const certificationCardData = useMemo(() => {
return (
<Radio.Group
className="h-max-100 overflow-y-auto overflow-x-hidden"
value={selectedCertification}>
{certifications.map((certificate) => {
const tagSrc = getTagImageSrc(certificate.style?.iconURL ?? '');
const title = getEntityName(certificate);
const { id, fullyQualifiedName, description } = certificate;
const [paging, setPaging] = useState<Paging>({} as Paging);
const [currentPage, setCurrentPage] = useState(1);
return (
<div
className="certification-card-item cursor-pointer"
key={id}
style={{ cursor: 'pointer' }}
onClick={() => {
setSelectedCertification(fullyQualifiedName ?? '');
}}>
<Radio
className="certification-radio-top-right"
data-testid={`radio-btn-${fullyQualifiedName}`}
value={fullyQualifiedName}
/>
<div className="certification-card-content">
{tagSrc ? (
<img alt={title} src={tagSrc} />
) : (
<div className="certification-icon">
<CertificationIcon height={28} width={28} />
</div>
)}
<div>
<Typography.Paragraph className="m-b-0 font-regular text-xs text-grey-body">
{title}
</Typography.Paragraph>
<Typography.Paragraph className="m-b-0 font-regular text-xs text-grey-muted">
{stringToHTML(description)}
</Typography.Paragraph>
</div>
</div>
</div>
);
})}
</Radio.Group>
);
}, [certifications, selectedCertification]);
const getCertificationData = async (page = 1, append = false) => {
if (page === 1) {
setIsLoadingCertificationData(true);
} else {
setHasContentLoading(true);
}
try {
const response = await getTags({
parent: 'Certification',
limit: 50,
after: page > 1 ? paging.after : undefined,
});
const { data, paging: newPaging } = response;
// Sort certifications with Gold, Silver, Bronze first (only for initial load)
const sortedData =
page === 1
? [...data].sort((a, b) => {
const order: Record<string, number> = {
Gold: 0,
Silver: 1,
Bronze: 2,
};
const aName = getEntityName(a);
const bName = getEntityName(b);
const aOrder = order[aName] ?? 3;
const bOrder = order[bName] ?? 3;
return aOrder - bOrder;
})
: data;
if (append) {
setCertifications((prev) => [...prev, ...sortedData]);
} else {
setCertifications(sortedData);
}
setPaging(newPaging);
setCurrentPage(page);
} catch (err) {
showErrorToast(
err as AxiosError,
t('server.entity-fetch-error', {
entity: t('label.certification-plural-lowercase'),
})
);
} finally {
setIsLoadingCertificationData(false);
setHasContentLoading(false);
}
};
const handleScroll = async (e: React.UIEvent<HTMLDivElement>) => {
const { currentTarget } = e;
const isAtBottom =
currentTarget.scrollTop + currentTarget.offsetHeight >=
currentTarget.scrollHeight - 1; // -1 for precision tolerance
if (isAtBottom && paging.after && !hasContentLoading) {
await getCertificationData(currentPage + 1, true);
}
};
const updateCertificationData = async (value?: string) => {
setIsLoadingCertificationData(true);
@ -98,43 +124,60 @@ const Certification = ({
setIsLoadingCertificationData(false);
popoverRef.current?.close();
};
const getCertificationData = async () => {
setIsLoadingCertificationData(true);
try {
const { data } = await getTags({
parent: 'Certification',
limit: 50,
});
// Sort certifications with Gold, Silver, Bronze first
const sortedData = [...data].sort((a, b) => {
const order: Record<string, number> = {
Gold: 0,
Silver: 1,
Bronze: 2,
};
const certificationCardData = useMemo(() => {
return (
<div
className="h-max-100 overflow-y-auto overflow-x-hidden"
onScroll={handleScroll}>
<Radio.Group className="w-full" value={selectedCertification}>
{certifications.map((certificate) => {
const tagSrc = getTagImageSrc(certificate.style?.iconURL ?? '');
const title = getEntityName(certificate);
const { id, fullyQualifiedName, description } = certificate;
const aName = getEntityName(a);
const bName = getEntityName(b);
const aOrder = order[aName] ?? 3;
const bOrder = order[bName] ?? 3;
return aOrder - bOrder;
});
setCertifications(sortedData);
} catch (err) {
showErrorToast(
err as AxiosError,
t('server.entity-fetch-error', {
entity: t('label.certification-plural-lowercase'),
})
);
} finally {
setIsLoadingCertificationData(false);
}
};
return (
<div
className="certification-card-item cursor-pointer"
key={id}
style={{ cursor: 'pointer' }}
onClick={() => {
setSelectedCertification(fullyQualifiedName ?? '');
}}>
<Radio
className="certification-radio-top-right"
data-testid={`radio-btn-${fullyQualifiedName}`}
value={fullyQualifiedName}
/>
<div className="certification-card-content">
{tagSrc ? (
<img alt={title} src={tagSrc} />
) : (
<div className="certification-icon">
<CertificationIcon height={28} width={28} />
</div>
)}
<div>
<Typography.Paragraph className="m-b-0 font-regular text-xs text-grey-body">
{title}
</Typography.Paragraph>
<Typography.Paragraph className="m-b-0 font-regular text-xs text-grey-muted">
{stringToHTML(description)}
</Typography.Paragraph>
</div>
</div>
</div>
);
})}
</Radio.Group>
{hasContentLoading && (
<div className="flex justify-center p-2">
<Loader size="small" />
</div>
)}
</div>
);
}, [certifications, selectedCertification, hasContentLoading, handleScroll]);
const handleCloseCertification = async () => {
popoverRef.current?.close();
@ -143,16 +186,19 @@ const Certification = ({
const onOpenChange = (visible: boolean) => {
if (visible) {
getCertificationData();
getCertificationData(1);
setSelectedCertification(currentCertificate);
setCurrentPage(1);
setPaging({} as Paging);
} else {
setSelectedCertification('');
setCertifications([]);
}
};
useEffect(() => {
if (popoverProps?.open && certifications.length === 0) {
getCertificationData();
getCertificationData(1);
}
}, [popoverProps?.open]);

View File

@ -226,7 +226,7 @@ describe('ContractDetail', () => {
});
it('should display contract actions', () => {
const { getByTestId, debug } = render(
const { getByTestId } = render(
<ContractDetail
contract={mockContract}
onDelete={mockOnDelete}
@ -234,7 +234,48 @@ describe('ContractDetail', () => {
/>,
{ wrapper: MemoryRouter }
);
debug();
expect(getByTestId('manage-contract-actions')).toBeInTheDocument();
});
it('should not display contract created by or created at if data is not present', () => {
const { getByTestId, queryByTestId } = render(
<ContractDetail
contract={mockContract}
onDelete={mockOnDelete}
onEdit={mockOnEdit}
/>,
{ wrapper: MemoryRouter }
);
expect(
queryByTestId('contract-created-by-label')
).not.toBeInTheDocument();
expect(
queryByTestId('contract-created-at-label')
).not.toBeInTheDocument();
expect(getByTestId('manage-contract-actions')).toBeInTheDocument();
});
it('should display contract created by or created at if data is present', () => {
const { getByTestId, queryByTestId } = render(
<ContractDetail
contract={{
...mockContract,
createdBy: 'admin',
createdAt: 1758556706799,
}}
onDelete={mockOnDelete}
onEdit={mockOnEdit}
/>,
{ wrapper: MemoryRouter }
);
expect(queryByTestId('contract-created-by-label')).toBeInTheDocument();
expect(queryByTestId('contract-created-at-label')).toBeInTheDocument();
expect(getByTestId('manage-contract-actions')).toBeInTheDocument();
});

View File

@ -37,6 +37,7 @@ import { ReactComponent as SettingIcon } from '../../../assets/svg/ic-settings-v
import { ReactComponent as DeleteIcon } from '../../../assets/svg/ic-trash.svg';
import {
CONTRACT_DATE_TIME_FORMAT,
DataContractMode,
DATA_CONTRACT_ACTION_DROPDOWN_KEY,
} from '../../../constants/DataContract.constants';
@ -53,6 +54,7 @@ import {
downloadContractYamlFile,
getConstraintStatus,
} from '../../../utils/DataContract/DataContractUtils';
import { customFormatDateTime } from '../../../utils/date-time/DateTimeUtils';
import { getEntityName } from '../../../utils/EntityUtils';
import { pruneEmptyChildren } from '../../../utils/TableUtils';
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
@ -271,6 +273,55 @@ const ContractDetail: React.FC<{
</div>
</Col>
<Col className="d-flex items-center gap-2" span={24}>
{contract.createdBy && (
<>
<div className="d-flex items-center">
<Typography.Text
className="contract-sub-header-title"
data-testid="contract-created-by-label">
{`${t('label.created-by')} : `}
</Typography.Text>
<OwnerLabel
owners={[
{ name: contract.createdBy, type: 'user', id: '' },
]}
/>
</div>
<Divider
className="self-center vertical-divider"
type="vertical"
/>
</>
)}
{contract.createdAt && (
<>
<div className="d-flex items-center">
<Typography.Text
className="contract-sub-header-title"
data-testid="contract-created-at-label">
{`${t('label.created-at')} : `}
</Typography.Text>
<Typography.Text
className="contract-sub-header-value"
data-testid="contract-created-at-value">
{customFormatDateTime(
contract.createdAt,
CONTRACT_DATE_TIME_FORMAT
)}
</Typography.Text>
</div>
<Divider
className="self-center vertical-divider"
type="vertical"
/>
</>
)}
<div className="d-flex items-center">
<Typography.Text
className="contract-sub-header-title"

View File

@ -41,6 +41,11 @@
color: @grey-700;
font-weight: 400;
}
.contract-sub-header-value {
font-size: 14px;
color: @grey-900;
font-weight: 500;
}
.contract-action-container {
display: flex;

View File

@ -51,7 +51,7 @@ const ContractSLA: React.FC<{
});
}
if (contract.sla?.refreshFrequency) {
if (contract.sla?.availabilityTime) {
slaList.push({
key: DATA_CONTRACT_SLA.TIME_AVAILABILITY,
label: (
@ -67,7 +67,7 @@ const ContractSLA: React.FC<{
});
}
if (contract.sla?.refreshFrequency) {
if (contract.sla?.maxLatency) {
slaList.push({
key: DATA_CONTRACT_SLA.MAX_LATENCY,
label: (
@ -85,7 +85,7 @@ const ContractSLA: React.FC<{
});
}
if (contract.sla?.refreshFrequency) {
if (contract.sla?.retention) {
slaList.push({
key: DATA_CONTRACT_SLA.RETENTION,
label: (

View File

@ -0,0 +1,440 @@
/*
* Copyright 2025 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { render, screen } from '@testing-library/react';
import {
DataContract,
MaxLatencyUnit,
RefreshFrequencyUnit,
RetentionUnit,
} from '../../../generated/entity/data/dataContract';
import { MOCK_DATA_CONTRACT } from '../../../mocks/DataContract.mock';
import ContractSLA from './ContractSLA.component';
jest.mock('../../../utils/CommonUtils', () => ({
Transi18next: ({ i18nKey, values }: any) => (
<span>
{i18nKey} - {values?.label}: {values?.data}
</span>
),
}));
jest.mock('../../../assets/svg/ic-check-circle-2.svg', () => ({
ReactComponent: () => <svg data-testid="check-icon" />,
}));
describe('ContractSLA Component', () => {
it('should render null when contract.sla is empty', () => {
const contract: DataContract = {
fullyQualifiedName: 'test.contract',
name: 'Test Contract',
sla: {},
} as DataContract;
const { container } = render(<ContractSLA contract={contract} />);
expect(container.firstChild).toBeNull();
});
it('should render null when contract.sla is undefined', () => {
const contract: DataContract = {
fullyQualifiedName: 'test.contract',
name: 'Test Contract',
} as DataContract;
const { container } = render(<ContractSLA contract={contract} />);
expect(container.firstChild).toBeNull();
});
it('should render SLA card with refresh frequency', () => {
const contract: DataContract = {
fullyQualifiedName: 'test.contract',
name: 'Test Contract',
id: MOCK_DATA_CONTRACT.id,
entity: MOCK_DATA_CONTRACT.entity,
sla: {
refreshFrequency: {
interval: 24,
unit: RefreshFrequencyUnit.Hour,
},
},
} as DataContract;
render(<ContractSLA contract={contract} />);
expect(screen.getByTestId('contract-sla-card')).toBeInTheDocument();
expect(
screen.getByText('label.service-level-agreement')
).toBeInTheDocument();
expect(
screen.getByText(
'message.freshness-sla-description - label.freshness: 24 hour'
)
).toBeInTheDocument();
expect(screen.getByTestId('check-icon')).toBeInTheDocument();
});
it('should render SLA card with availability time', () => {
const contract: DataContract = {
fullyQualifiedName: 'test.contract',
name: 'Test Contract',
sla: {
availabilityTime: '09:00 UTC',
},
} as DataContract;
render(<ContractSLA contract={contract} />);
expect(screen.getByTestId('contract-sla-card')).toBeInTheDocument();
expect(
screen.getByText(
'message.completeness-sla-description - label.completeness: 09:00 UTC'
)
).toBeInTheDocument();
});
it('should render SLA card with max latency', () => {
const contract: DataContract = {
fullyQualifiedName: 'test.contract',
name: 'Test Contract',
id: MOCK_DATA_CONTRACT.id,
entity: MOCK_DATA_CONTRACT.entity,
sla: {
maxLatency: {
value: 5,
unit: MaxLatencyUnit.Minute,
},
},
} as DataContract;
render(<ContractSLA contract={contract} />);
expect(screen.getByTestId('contract-sla-card')).toBeInTheDocument();
expect(
screen.getByText(
'message.latency-sla-description - label.latency: 5 minute'
)
).toBeInTheDocument();
});
it('should render SLA card with retention', () => {
const contract: DataContract = {
fullyQualifiedName: 'test.contract',
name: 'Test Contract',
id: MOCK_DATA_CONTRACT.id,
entity: MOCK_DATA_CONTRACT.entity,
sla: {
retention: {
period: 30,
unit: RetentionUnit.Day,
},
},
} as DataContract;
render(<ContractSLA contract={contract} />);
expect(screen.getByTestId('contract-sla-card')).toBeInTheDocument();
expect(
screen.getByText(
'message.retention-sla-description - label.retention: 30 day'
)
).toBeInTheDocument();
});
it('should render all SLA items when all properties are present', () => {
const contract: DataContract = {
fullyQualifiedName: 'test.contract',
name: 'Test Contract',
id: MOCK_DATA_CONTRACT.id,
entity: MOCK_DATA_CONTRACT.entity,
sla: {
refreshFrequency: {
interval: 24,
unit: RefreshFrequencyUnit.Hour,
},
availabilityTime: '09:00 UTC',
maxLatency: {
value: 5,
unit: MaxLatencyUnit.Minute,
},
retention: {
period: 30,
unit: RetentionUnit.Day,
},
},
} as DataContract;
render(<ContractSLA contract={contract} />);
expect(screen.getByTestId('contract-sla-card')).toBeInTheDocument();
// Check all SLA items are rendered
expect(
screen.getByText(
'message.freshness-sla-description - label.freshness: 24 hour'
)
).toBeInTheDocument();
expect(
screen.getByText(
'message.completeness-sla-description - label.completeness: 09:00 UTC'
)
).toBeInTheDocument();
expect(
screen.getByText(
'message.latency-sla-description - label.latency: 5 minute'
)
).toBeInTheDocument();
expect(
screen.getByText(
'message.retention-sla-description - label.retention: 30 day'
)
).toBeInTheDocument();
// Check that all check icons are rendered
const checkIcons = screen.getAllByTestId('check-icon');
expect(checkIcons).toHaveLength(4);
});
it('should render only provided SLA items', () => {
const contract: DataContract = {
fullyQualifiedName: 'test.contract',
name: 'Test Contract',
id: MOCK_DATA_CONTRACT.id,
entity: MOCK_DATA_CONTRACT.entity,
sla: {
refreshFrequency: {
interval: 12,
unit: RefreshFrequencyUnit.Hour,
},
retention: {
period: 7,
unit: RetentionUnit.Day,
},
},
} as DataContract;
render(<ContractSLA contract={contract} />);
expect(screen.getByTestId('contract-sla-card')).toBeInTheDocument();
// Check only provided items are rendered
expect(
screen.getByText(
'message.freshness-sla-description - label.freshness: 12 hour'
)
).toBeInTheDocument();
expect(
screen.getByText(
'message.retention-sla-description - label.retention: 7 day'
)
).toBeInTheDocument();
// Check that availability and latency are not rendered
expect(
screen.queryByText(/message.completeness-sla-description/)
).not.toBeInTheDocument();
expect(
screen.queryByText(/message.latency-sla-description/)
).not.toBeInTheDocument();
// Check correct number of check icons
const checkIcons = screen.getAllByTestId('check-icon');
expect(checkIcons).toHaveLength(2);
});
it('should handle uppercase units and convert them to lowercase', () => {
const contract: DataContract = {
fullyQualifiedName: 'test.contract',
name: 'Test Contract',
id: MOCK_DATA_CONTRACT.id,
entity: MOCK_DATA_CONTRACT.entity,
sla: {
refreshFrequency: {
interval: 1,
unit: RefreshFrequencyUnit.Day,
},
maxLatency: {
value: 10,
unit: MaxLatencyUnit.Minute,
},
},
} as DataContract;
render(<ContractSLA contract={contract} />);
expect(
screen.getByText(
'message.freshness-sla-description - label.freshness: 1 day'
)
).toBeInTheDocument();
expect(
screen.getByText(
'message.latency-sla-description - label.latency: 10 minute'
)
).toBeInTheDocument();
});
it('should have correct CSS classes applied', () => {
const contract: DataContract = {
fullyQualifiedName: 'test.contract',
name: 'Test Contract',
id: MOCK_DATA_CONTRACT.id,
entity: MOCK_DATA_CONTRACT.entity,
sla: {
refreshFrequency: {
interval: 24,
unit: RefreshFrequencyUnit.Hour,
},
},
} as DataContract;
const { container } = render(<ContractSLA contract={contract} />);
expect(container.querySelector('.contract-card-items')).toBeInTheDocument();
expect(
container.querySelector('.contract-card-header-container')
).toBeInTheDocument();
expect(
container.querySelector('.contract-card-header')
).toBeInTheDocument();
expect(
container.querySelector('.contract-dash-separator')
).toBeInTheDocument();
expect(container.querySelector('.sla-item-container')).toBeInTheDocument();
expect(container.querySelector('.sla-item')).toBeInTheDocument();
expect(container.querySelector('.sla-icon')).toBeInTheDocument();
expect(container.querySelector('.sla-description')).toBeInTheDocument();
});
it('should render SLA items in the correct order', () => {
const contract: DataContract = {
fullyQualifiedName: 'test.contract',
name: 'Test Contract',
id: MOCK_DATA_CONTRACT.id,
entity: MOCK_DATA_CONTRACT.entity,
sla: {
retention: {
period: 30,
unit: RetentionUnit.Day,
},
maxLatency: {
value: 5,
unit: MaxLatencyUnit.Minute,
},
availabilityTime: '09:00 UTC',
refreshFrequency: {
interval: 24,
unit: RefreshFrequencyUnit.Hour,
},
},
} as DataContract;
const { container } = render(<ContractSLA contract={contract} />);
const slaDescriptions = container.querySelectorAll('.sla-description');
// Order should be: refreshFrequency, availabilityTime, maxLatency, retention
expect(slaDescriptions[0]).toHaveTextContent(
'message.freshness-sla-description'
);
expect(slaDescriptions[1]).toHaveTextContent(
'message.completeness-sla-description'
);
expect(slaDescriptions[2]).toHaveTextContent(
'message.latency-sla-description'
);
expect(slaDescriptions[3]).toHaveTextContent(
'message.retention-sla-description'
);
});
describe('Edge Cases', () => {
it('should handle zero values correctly', () => {
const contract: DataContract = {
fullyQualifiedName: 'test.contract',
name: 'Test Contract',
id: MOCK_DATA_CONTRACT.id,
entity: MOCK_DATA_CONTRACT.entity,
sla: {
refreshFrequency: {
interval: 0,
unit: RefreshFrequencyUnit.Hour,
},
maxLatency: {
value: 0,
unit: MaxLatencyUnit.Minute,
},
},
} as DataContract;
render(<ContractSLA contract={contract} />);
expect(
screen.getByText(
'message.freshness-sla-description - label.freshness: 0 hour'
)
).toBeInTheDocument();
expect(
screen.getByText(
'message.latency-sla-description - label.latency: 0 minute'
)
).toBeInTheDocument();
});
it('should handle empty string for availabilityTime', () => {
const contract: DataContract = {
fullyQualifiedName: 'test.contract',
name: 'Test Contract',
sla: {
availabilityTime: '',
},
} as DataContract;
const { container } = render(<ContractSLA contract={contract} />);
// Should not render anything as availabilityTime is empty
expect(container.firstChild).toBeNull();
});
it('should handle partial SLA objects with undefined properties', () => {
const contract: DataContract = {
fullyQualifiedName: 'test.contract',
name: 'Test Contract',
sla: {
refreshFrequency: undefined,
availabilityTime: '09:00 UTC',
maxLatency: undefined,
retention: undefined,
},
} as DataContract;
render(<ContractSLA contract={contract} />);
// Only availabilityTime should be rendered
expect(screen.getByTestId('contract-sla-card')).toBeInTheDocument();
expect(
screen.getByText(
'message.completeness-sla-description - label.completeness: 09:00 UTC'
)
).toBeInTheDocument();
// Check only one check icon is rendered
const checkIcons = screen.getAllByTestId('check-icon');
expect(checkIcons).toHaveLength(1);
});
});
});

View File

@ -26,15 +26,17 @@ import moment from 'moment';
import { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as LeftOutlined } from '../../../assets/svg/left-arrow.svg';
import { SLA_AVAILABILITY_TIME_FORMAT } from '../../../constants/DataContract.constants';
import {
MAX_LATENCY_UNITS,
REFRESH_FREQUENCY_UNITS,
RETENTION_UNITS,
SLA_AVAILABILITY_TIME_FORMAT,
} from '../../../constants/DataContract.constants';
import {
ContractSLA,
DataContract,
MaxLatencyUnit,
RefreshFrequencyUnit,
RetentionUnit,
} from '../../../generated/entity/data/dataContract';
import { enumToSelectOptions } from '../../../utils/DataContract/DataContractUtils';
import { generateSelectOptionsFromString } from '../../../utils/DataContract/DataContractUtils';
import './contract-sla-form-tab.less';
export const ContractSLAFormTab: React.FC<{
@ -52,9 +54,11 @@ export const ContractSLAFormTab: React.FC<{
REFRESH_FREQUENCY_UNIT_OPTIONS,
} = useMemo(() => {
return {
REFRESH_FREQUENCY_UNIT_OPTIONS: enumToSelectOptions(RefreshFrequencyUnit),
RETENTION_UNIT_OPTIONS: enumToSelectOptions(RetentionUnit),
MAX_LATENCY_OPTIONS: enumToSelectOptions(MaxLatencyUnit),
REFRESH_FREQUENCY_UNIT_OPTIONS: generateSelectOptionsFromString(
REFRESH_FREQUENCY_UNITS
),
RETENTION_UNIT_OPTIONS: generateSelectOptionsFromString(RETENTION_UNITS),
MAX_LATENCY_OPTIONS: generateSelectOptionsFromString(MAX_LATENCY_UNITS),
};
}, []);
@ -166,7 +170,7 @@ export const ContractSLAFormTab: React.FC<{
label={t('label.unit')}
name="refresh_frequency_unit">
<Select
// allowClear
allowClear
data-testid="refresh-frequency-unit-select"
options={REFRESH_FREQUENCY_UNIT_OPTIONS}
popupClassName="refresh-frequency-unit-select"
@ -210,6 +214,7 @@ export const ContractSLAFormTab: React.FC<{
label={t('label.unit')}
name="max_latency_unit">
<Select
allowClear
data-testid="max-latency-unit-select"
options={MAX_LATENCY_OPTIONS}
popupClassName="max-latency-unit-select"
@ -274,6 +279,7 @@ export const ContractSLAFormTab: React.FC<{
label={t('label.unit')}
name="retention_unit">
<Select
allowClear
data-testid="retention-unit-select"
options={RETENTION_UNIT_OPTIONS}
popupClassName="retention-unit-select"

View File

@ -94,7 +94,8 @@ const ExploreQuickFilters: FC<ExploreQuickFiltersProps> = ({
key,
'',
JSON.stringify(combinedQueryFilter),
independent
independent,
showDeleted
),
key === TIER_FQN_KEY
? getTags({ parent: 'Tier', limit: 50 })
@ -159,7 +160,8 @@ const ExploreQuickFilters: FC<ExploreQuickFiltersProps> = ({
key,
value,
JSON.stringify(combinedQueryFilter),
independent
independent,
showDeleted
);
const buckets = res.data.aggregations[`sterms#${key}`].buckets;

View File

@ -319,4 +319,185 @@ describe('Test IngestionRecentRun component', () => {
expect(getRunHistoryForPipeline).toHaveBeenCalledTimes(2);
expect(mockHandlePipelineIdToFetchStatus).toHaveBeenCalled();
});
describe('Timestamp Sorting Logic', () => {
it('should sort runs by timestamp in ascending order and show last 5 runs', async () => {
const unorderedRuns = [
{
runId: 'run-3',
pipelineState: 'success',
startDate: 1667307000,
timestamp: 1667307000,
endDate: 1667307003,
},
{
runId: 'run-1',
pipelineState: 'failed',
startDate: 1667301000,
timestamp: 1667301000,
endDate: 1667301003,
},
{
runId: 'run-5',
pipelineState: 'success',
startDate: 1667309000,
timestamp: 1667309000,
endDate: 1667309003,
},
{
runId: 'run-2',
pipelineState: 'partialSuccess',
startDate: 1667304000,
timestamp: 1667304000,
endDate: 1667304003,
},
{
runId: 'run-4',
pipelineState: 'running',
startDate: 1667308000,
timestamp: 1667308000,
endDate: 1667308003,
},
{
runId: 'run-6',
pipelineState: 'queued',
startDate: 1667310000,
timestamp: 1667310000,
endDate: 1667310003,
},
{
runId: 'run-7',
pipelineState: 'success',
startDate: 1667311000,
timestamp: 1667311000,
endDate: 1667311003,
},
];
(getRunHistoryForPipeline as jest.Mock).mockResolvedValueOnce({
data: unorderedRuns,
paging: { total: 7 },
});
await act(async () => {
render(<IngestionRecentRuns ingestion={mockIngestion} />);
});
const runs = await screen.findAllByTestId('pipeline-status');
expect(runs).toHaveLength(5);
const latestRun = await screen.findByText(/Success/);
expect(latestRun).toBeInTheDocument();
});
it('should handle runs with missing timestamps gracefully by treating them as 0', async () => {
const runsWithMissingTimestamps = [
{
runId: 'run-with-timestamp',
pipelineState: 'success',
startDate: 1667307000,
timestamp: 1667307000,
endDate: 1667307003,
},
{
runId: 'run-without-timestamp',
pipelineState: 'failed',
startDate: 1667301000,
endDate: 1667301003,
},
{
runId: 'run-with-null-timestamp',
pipelineState: 'partialSuccess',
startDate: 1667304000,
timestamp: null,
endDate: 1667304003,
},
];
(getRunHistoryForPipeline as jest.Mock).mockResolvedValueOnce({
data: runsWithMissingTimestamps,
paging: { total: 3 },
});
await act(async () => {
render(<IngestionRecentRuns ingestion={mockIngestion} />);
});
const runs = await screen.findAllByTestId('pipeline-status');
expect(runs).toHaveLength(3);
const latestRun = await screen.findByText(/Success/);
expect(latestRun).toBeInTheDocument();
});
it('should sort appRuns by timestamp when provided', async () => {
const unorderedAppRuns = [
{
runId: 'app-run-2',
status: 'Success',
startTime: 1667307000,
timestamp: 1667307000,
endTime: 1667307003,
appId: 'app-2',
},
{
runId: 'app-run-1',
status: 'Failed',
startTime: 1667301000,
timestamp: 1667301000,
endTime: 1667301003,
appId: 'app-1',
},
{
runId: 'app-run-3',
status: 'Running',
startTime: 1667309000,
timestamp: 1667309000,
endTime: 1667309003,
appId: 'app-3',
},
];
await act(async () => {
render(
<IngestionRecentRuns appRuns={unorderedAppRuns} fetchStatus={false} />
);
});
const runs = await screen.findAllByTestId('pipeline-status');
expect(runs).toHaveLength(3);
const latestRun = await screen.findByText(/Running/);
expect(latestRun).toBeInTheDocument();
});
it('should limit results to maximum 5 runs after sorting', async () => {
const manyRuns = Array.from({ length: 10 }, (_, i) => ({
runId: `run-${i}`,
pipelineState: 'success',
startDate: 1667300000 + i * 1000,
timestamp: 1667300000 + i * 1000,
endDate: 1667300003 + i * 1000,
}));
(getRunHistoryForPipeline as jest.Mock).mockResolvedValueOnce({
data: manyRuns,
paging: { total: 10 },
});
await act(async () => {
render(<IngestionRecentRuns ingestion={mockIngestion} />);
});
const runs = await screen.findAllByTestId('pipeline-status');
expect(runs).toHaveLength(5);
});
});
});

View File

@ -65,7 +65,10 @@ export const IngestionRecentRuns = <
queryParams
);
const runs = response.data.splice(0, 5).reverse() ?? [];
const runs =
response.data
.sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0))
.slice(-5) ?? [];
setRecentRunStatus(
(runs.length === 0 && ingestionPipeline?.pipelineStatuses
@ -86,7 +89,11 @@ export const IngestionRecentRuns = <
useEffect(() => {
if (!isEmpty(appRuns)) {
setRecentRunStatus(appRuns?.splice(0, 5).reverse() ?? []);
setRecentRunStatus(
appRuns
?.sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0))
.slice(-5) ?? []
);
}
}, [appRuns]);

View File

@ -37,7 +37,6 @@ import {
getFilteredSchema,
getUISchemaWithNestedDefaultFilterFieldsHidden,
} from '../../../../utils/ServiceConnectionUtils';
import { shouldTestConnection } from '../../../../utils/ServiceUtils';
import AirflowMessageBanner from '../../../common/AirflowMessageBanner/AirflowMessageBanner';
import BooleanFieldTemplate from '../../../common/Form/JSONSchema/JSONSchemaTemplate/BooleanFieldTemplate';
import WorkflowArrayFieldTemplate from '../../../common/Form/JSONSchema/JSONSchemaTemplate/WorkflowArrayFieldTemplate';
@ -61,17 +60,10 @@ const ConnectionConfigForm = ({
const { inlineAlertDetails } = useApplicationStore();
const { t } = useTranslation();
const [ingestionRunner, setIngestionRunner] = useState<string | undefined>();
const { isAirflowAvailable, platform } = useAirflowStatus();
const allowTestConn = useMemo(() => {
return shouldTestConnection(serviceType);
}, [serviceType]);
const [hasTestedConnection, setHasTestedConnection] = useState(
!isAirflowAvailable || !allowTestConn || disableTestConnection
);
const formRef = useRef<Form<ConfigData>>(null);
const { isAirflowAvailable, platform } = useAirflowStatus();
const [hostIp, setHostIp] = useState<string>();
const fetchHostIp = async () => {
@ -161,10 +153,6 @@ const ConnectionConfigForm = ({
}
}, [formRef.current?.state?.formData]);
const handleTestConnection = () => {
setHasTestedConnection(true);
};
return (
<Fragment>
<AirflowMessageBanner />
@ -173,7 +161,6 @@ const ConnectionConfigForm = ({
cancelText={cancelText ?? ''}
fields={customFields}
formData={validConfig}
hasTestedConnection={hasTestedConnection}
okText={okText ?? ''}
ref={formRef}
schema={schemaWithoutDefaultFilterPatternFields}
@ -204,18 +191,19 @@ const ConnectionConfigForm = ({
type="info"
/>
)}
{!isEmpty(connSch.schema) && (
<TestConnection
connectionType={serviceType}
getData={() => formRef.current?.state?.formData}
hostIp={hostIp}
isTestingDisabled={disableTestConnection}
serviceCategory={serviceCategory}
serviceName={data?.name}
onTestConnection={handleTestConnection}
onValidateFormRequiredFields={handleRequiredFieldsValidation}
/>
)}
{!isEmpty(connSch.schema) &&
isAirflowAvailable &&
formRef.current?.state?.formData && (
<TestConnection
connectionType={serviceType}
getData={() => formRef.current?.state?.formData}
hostIp={hostIp}
isTestingDisabled={disableTestConnection}
serviceCategory={serviceCategory}
serviceName={data?.name}
onValidateFormRequiredFields={handleRequiredFieldsValidation}
/>
)}
{!isUndefined(inlineAlertDetails) && (
<InlineAlert alertClassName="m-t-xs" {...inlineAlertDetails} />
)}

View File

@ -44,7 +44,6 @@ export interface Props extends FormProps {
onCancel?: () => void;
useSelectWidget?: boolean;
capitalizeOptionLabel?: boolean;
hasTestedConnection?: boolean;
}
const FormBuilder = forwardRef<Form, Props>(
@ -65,7 +64,6 @@ const FormBuilder = forwardRef<Form, Props>(
useSelectWidget = false,
capitalizeOptionLabel = false,
children,
hasTestedConnection,
...props
},
ref
@ -106,9 +104,6 @@ const FormBuilder = forwardRef<Form, Props>(
};
const submitButton = useMemo(() => {
if (hasTestedConnection === false) {
return null;
}
if (status === 'waiting') {
return (
<Button
@ -139,7 +134,7 @@ const FormBuilder = forwardRef<Form, Props>(
</Button>
);
}
}, [status, isLoading, okText, hasTestedConnection]);
}, [status, isLoading, okText]);
return (
<Form

View File

@ -23,7 +23,6 @@ export interface TestConnectionProps {
serviceName?: string;
shouldValidateForm?: boolean;
onValidateFormRequiredFields?: () => boolean;
onTestConnection?: () => void;
hostIp?: string;
}

View File

@ -70,7 +70,6 @@ const TestConnection: FC<TestConnectionProps> = ({
onValidateFormRequiredFields,
shouldValidateForm = true,
showDetails = true,
onTestConnection,
hostIp,
}) => {
const { t } = useTranslation();
@ -373,8 +372,6 @@ const TestConnection: FC<TestConnectionProps> = ({
if (workflowId) {
await handleDeleteWorkflow(workflowId);
}
} finally {
onTestConnection?.();
}
};

View File

@ -15,6 +15,8 @@ import { BarProps } from 'recharts';
import { EntityReferenceFields } from '../enums/AdvancedSearch.enum';
import jsonLogicSearchClassBase from '../utils/JSONLogicSearchClassBase';
export const CONTRACT_DATE_TIME_FORMAT = 'MM/dd/yyyy, h:mma';
export enum DataContractMode {
YAML,
UI,
@ -75,3 +77,7 @@ export const DATA_CONTRACT_EXECUTION_CHART_COMMON_PROPS: {
maxBarSize: 12,
radius: [6, 6, 0, 0],
};
export const MAX_LATENCY_UNITS = ['minute', 'hour', 'day'];
export const REFRESH_FREQUENCY_UNITS = ['hour', 'day', 'week', 'month', 'year'];
export const RETENTION_UNITS = ['day', 'week', 'month', 'year'];

View File

@ -316,6 +316,7 @@
"create-new-test-suite": "Neuen Testsuite erstellen",
"create-widget": "Widget erstellen",
"created-a-task-lowercase": "hat eine Aufgabe erstellt",
"created-at": "Created at",
"created-by": "Erstellt von",
"created-by-me": "Von mir erstellt",
"created-date": "Erstellungsdatum",
@ -1108,6 +1109,7 @@
"nodes-per-layer": "Knoten pro Ebene",
"non-partitioned": "Nicht partitioniert",
"none": "Keine/r",
"not-contain-plural": "Enthält nicht",
"not-covered": "Nicht abgedeckt",
"not-found-lowercase": "nicht gefunden",
"not-null": "Nicht null",

View File

@ -316,6 +316,7 @@
"create-new-test-suite": "Create new test suite",
"create-widget": "Create Widget",
"created-a-task-lowercase": "created a task",
"created-at": "Created at",
"created-by": "Created By",
"created-by-me": "Created by me",
"created-date": "Created Date",
@ -1108,6 +1109,7 @@
"nodes-per-layer": "Nodes Per Layer",
"non-partitioned": "Non-partitioned",
"none": "None",
"not-contain-plural": "Not Contains",
"not-covered": "Not Covered",
"not-found-lowercase": "not found",
"not-null": "Not Null",

View File

@ -316,6 +316,7 @@
"create-new-test-suite": "Crear nueva suite de tests",
"create-widget": "Crear Widget",
"created-a-task-lowercase": "creó una tarea",
"created-at": "Created at",
"created-by": "Creado Por",
"created-by-me": "Creado por mí",
"created-date": "Fecha de creación",
@ -1108,6 +1109,7 @@
"nodes-per-layer": "Nodos por capa",
"non-partitioned": "Sin partición",
"none": "Ninguno",
"not-contain-plural": "No contiene",
"not-covered": "No cubierto",
"not-found-lowercase": "no encontrado",
"not-null": "No nulo",

View File

@ -316,6 +316,7 @@
"create-new-test-suite": "Créer un Nouvel Ensemble de Tests",
"create-widget": "Créer un Widget",
"created-a-task-lowercase": "a créé une tâche",
"created-at": "Created at",
"created-by": "Créé par",
"created-by-me": "Créé par moi",
"created-date": "Date de Création",
@ -1108,6 +1109,7 @@
"nodes-per-layer": "Nœuds par Couche",
"non-partitioned": "Aucune Partition",
"none": "Aucun·e",
"not-contain-plural": "Ne contient pas",
"not-covered": "Not Covered",
"not-found-lowercase": "non trouvé·e",
"not-null": "Non Null",

View File

@ -316,6 +316,7 @@
"create-new-test-suite": "Crear un novo conxunto de probas",
"create-widget": "Crear Widget",
"created-a-task-lowercase": "creouse unha tarefa",
"created-at": "Created at",
"created-by": "Creado por",
"created-by-me": "Creado por min",
"created-date": "Data de creación",
@ -1108,6 +1109,7 @@
"nodes-per-layer": "Nós por capa",
"non-partitioned": "Non particionado",
"none": "Ningún",
"not-contain-plural": "Non contén",
"not-covered": "Non cuberto",
"not-found-lowercase": "non atopado",
"not-null": "Non nulo",

View File

@ -316,6 +316,7 @@
"create-new-test-suite": "צור מערכת בדיקה חדשה",
"create-widget": "צור ווידג'ט",
"created-a-task-lowercase": "משימה נוצרה",
"created-at": "Created at",
"created-by": "נוצר על ידי",
"created-by-me": "נוצר על ידיי",
"created-date": "תאריך יצירה",
@ -1108,6 +1109,7 @@
"nodes-per-layer": "צמתים לשכבה",
"non-partitioned": "לא מחולק לחלקים",
"none": "אף אחד",
"not-contain-plural": "לא מכיל",
"not-covered": "Not Covered",
"not-found-lowercase": "לא נמצא",
"not-null": "לא ריק",

View File

@ -316,6 +316,7 @@
"create-new-test-suite": "新しいテストスイートを作成",
"create-widget": "ウィジェットを作成",
"created-a-task-lowercase": "タスクを作成しました",
"created-at": "Created at",
"created-by": "作成者",
"created-by-me": "自分が作成",
"created-date": "作成日",
@ -1108,6 +1109,7 @@
"nodes-per-layer": "レイヤーごとのノード数",
"non-partitioned": "非パーティション化",
"none": "なし",
"not-contain-plural": "含まない",
"not-covered": "未カバー",
"not-found-lowercase": "見つかりません",
"not-null": "NULLではない",

View File

@ -316,6 +316,7 @@
"create-new-test-suite": "새 테스트 스위트 생성",
"create-widget": "위젯 생성",
"created-a-task-lowercase": "작업 생성됨",
"created-at": "Created at",
"created-by": "생성자",
"created-by-me": "내가 생성함",
"created-date": "생성 날짜",
@ -1108,6 +1109,7 @@
"nodes-per-layer": "계층당 노드",
"non-partitioned": "비분할",
"none": "없음",
"not-contain-plural": "포함하지 않음",
"not-covered": "포함되지 않음",
"not-found-lowercase": "찾을 수 없음",
"not-null": "널이 아님",

View File

@ -316,6 +316,7 @@
"create-new-test-suite": "नवीन चाचणी संच तयार करा",
"create-widget": "विजेट तयार करा",
"created-a-task-lowercase": "कार्य तयार केले",
"created-at": "Created at",
"created-by": "द्वारे तयार केले",
"created-by-me": "माझ्याद्वारे तयार केले",
"created-date": "तयार केलेली तारीख",
@ -1108,6 +1109,7 @@
"nodes-per-layer": "प्रत्येक स्तरातील नोड्स",
"non-partitioned": "विभाजन नसलेले",
"none": "काहीही नाही",
"not-contain-plural": "अंतर्भूत नाही",
"not-covered": "आवृत नाही",
"not-found-lowercase": "सापडले नाही",
"not-null": "नॉट नल",

View File

@ -316,6 +316,7 @@
"create-new-test-suite": "Nieuwe testsuite maken",
"create-widget": "Widget maken",
"created-a-task-lowercase": "een taak aangemaakt",
"created-at": "Created at",
"created-by": "Aangemaakt door",
"created-by-me": "Aangemaakt door mij",
"created-date": "Aanmaakdatum",
@ -1108,6 +1109,7 @@
"nodes-per-layer": "Knooppunten per laag",
"non-partitioned": "Niet-gepartitioneerd",
"none": "Geen",
"not-contain-plural": "Bevat niet",
"not-covered": "Not Covered",
"not-found-lowercase": "niet gevonden",
"not-null": "Niet leeg",

View File

@ -316,6 +316,7 @@
"create-new-test-suite": "ایجاد مجموعه تست جدید",
"create-widget": "ایجاد ویجت",
"created-a-task-lowercase": "یک کار ایجاد شد",
"created-at": "Created at",
"created-by": "ایجاد شده توسط",
"created-by-me": "ایجاد شده توسط من",
"created-date": "تاریخ ایجاد",
@ -1108,6 +1109,7 @@
"nodes-per-layer": "گره‌ها در هر لایه",
"non-partitioned": "بدون تقسیم‌بندی",
"none": "هیچ‌کدام",
"not-contain-plural": "شامل نیست",
"not-covered": "Not Covered",
"not-found-lowercase": "یافت نشد",
"not-null": "غیر تهی",

View File

@ -316,6 +316,7 @@
"create-new-test-suite": "Criar novo conjunto de teste",
"create-widget": "Criar widget",
"created-a-task-lowercase": "criou uma tarefa",
"created-at": "Created at",
"created-by": "Criado Por",
"created-by-me": "Criado por mim",
"created-date": "Data de Criação",
@ -1108,6 +1109,7 @@
"nodes-per-layer": "Nós Por Camada",
"non-partitioned": "Não-particionado",
"none": "Nenhum",
"not-contain-plural": "Não contém",
"not-covered": "Não coberto",
"not-found-lowercase": "não encontrado",
"not-null": "Não Nulo",

View File

@ -316,6 +316,7 @@
"create-new-test-suite": "Criar novo conjunto de teste",
"create-widget": "Criar widget",
"created-a-task-lowercase": "criou uma tarefa",
"created-at": "Created at",
"created-by": "Criado Por",
"created-by-me": "Criado por mim",
"created-date": "Data de Criação",
@ -1108,6 +1109,7 @@
"nodes-per-layer": "Nós Por Camada",
"non-partitioned": "Não-particionado",
"none": "Nenhum",
"not-contain-plural": "Não contém",
"not-covered": "Not Covered",
"not-found-lowercase": "não encontrado",
"not-null": "Não Nulo",

View File

@ -316,6 +316,7 @@
"create-new-test-suite": "Создать новый набор тестов",
"create-widget": "Создать виджет",
"created-a-task-lowercase": "задача создана",
"created-at": "Created at",
"created-by": "Создано",
"created-by-me": "Создано мной",
"created-date": "Дата создания",
@ -1108,6 +1109,7 @@
"nodes-per-layer": "Узлы слоев",
"non-partitioned": "Неразделенный",
"none": "Отсутствует",
"not-contain-plural": "Не содержит",
"not-covered": "Не покрыто",
"not-found-lowercase": "не найдено",
"not-null": "Не пустой",

View File

@ -316,6 +316,7 @@
"create-new-test-suite": "สร้างชุดทดสอบใหม่",
"create-widget": "สร้างวิดเจ็ต",
"created-a-task-lowercase": "สร้างงาน",
"created-at": "Created at",
"created-by": "สร้างโดย",
"created-by-me": "สร้างโดยฉัน",
"created-date": "วันที่สร้าง",
@ -1108,6 +1109,7 @@
"nodes-per-layer": "โหนดต่อชั้น",
"non-partitioned": "ไม่มีการแบ่งส่วน",
"none": "ไม่มี",
"not-contain-plural": "ไม่ประกอบด้วย",
"not-covered": "ไม่ครอบคลุม",
"not-found-lowercase": "ไม่พบ",
"not-null": "ไม่เป็นค่าว่าง",

View File

@ -316,6 +316,7 @@
"create-new-test-suite": "Yeni test paketi oluştur",
"create-widget": "Widget oluştur",
"created-a-task-lowercase": "bir görev oluşturdu",
"created-at": "Created at",
"created-by": "Oluşturan",
"created-by-me": "Benim Oluşturduklarım",
"created-date": "Oluşturma Tarihi",
@ -1108,6 +1109,7 @@
"nodes-per-layer": "Katman Başına Düğüm Sayısı",
"non-partitioned": "Bölümlenmemiş",
"none": "Hiçbiri",
"not-contain-plural": "İçermez",
"not-covered": "Kapsanmayan",
"not-found-lowercase": "bulunamadı",
"not-null": "Boş Değil",

View File

@ -316,6 +316,7 @@
"create-new-test-suite": "创建新的质控测试",
"create-widget": "创建小组件",
"created-a-task-lowercase": "创建了一个任务",
"created-at": "Created at",
"created-by": "创建者",
"created-by-me": "由我创建",
"created-date": "创建日期",
@ -1108,6 +1109,7 @@
"nodes-per-layer": "每层节点数",
"non-partitioned": "未分区",
"none": "无",
"not-contain-plural": "不包含",
"not-covered": "Not Covered",
"not-found-lowercase": "未找到",
"not-null": "非空",

View File

@ -316,6 +316,7 @@
"create-new-test-suite": "建立新測試套件",
"create-widget": "建立小工具",
"created-a-task-lowercase": "已建立任務",
"created-at": "Created at",
"created-by": "建立者",
"created-by-me": "由我建立",
"created-date": "建立日期",
@ -1108,6 +1109,7 @@
"nodes-per-layer": "每層節點數",
"non-partitioned": "非分割",
"none": "無",
"not-contain-plural": "不包含",
"not-covered": "未涵蓋",
"not-found-lowercase": "找不到",
"not-null": "非空值",

View File

@ -80,6 +80,7 @@ const AddServicePage = () => {
const [saveServiceState, setSaveServiceState] =
useState<LoadingState>('initial');
const [activeField, setActiveField] = useState<string>('');
const slashedBreadcrumb = getAddServiceEntityBreadcrumb(serviceCategory);
const handleServiceTypeClick = (type: string) => {

View File

@ -168,7 +168,8 @@ export const getAggregateFieldOptions = (
field: string,
value: string,
q: string,
sourceFields?: string
sourceFields?: string,
deleted = false
) => {
const withWildCardValue = value
? `.*${escapeESReservedCharacters(value)}.*`
@ -179,6 +180,7 @@ export const getAggregateFieldOptions = (
value: withWildCardValue,
q,
sourceFields,
deleted,
};
return APIClient.get<SearchResponse<ExploreSearchIndex>>(

View File

@ -243,12 +243,12 @@ export const getContractExecutionMonthTicks = (
return Array.from(monthMap.values());
};
// Utility function to convert string enum to options array for Ant Design Select
export const enumToSelectOptions = <T extends Record<string, string>>(
enumObject: T
// Utility function to convert string to options array for Ant Design Select
export const generateSelectOptionsFromString = (
arrayItems: string[]
): Array<{ label: string; value: string }> => {
return Object.values(enumObject).map((value) => ({
return arrayItems.map((value) => ({
label: t(`label.${value}`),
value: value, // Use the enum value as the actual value (hour, day, week, etc.)
value: value, // Use the string value as the actual value (hour, day, week, etc.)
}));
};

View File

@ -200,41 +200,6 @@ export const extractTermKeys = (objects: QueryFieldInterface[]): string[] => {
return termKeys;
};
export const getSubLevelHierarchyKey = (
isDatabaseHierarchy = false,
filterField?: ExploreQuickFilterField[],
key?: EntityFields,
value?: string
) => {
const queryFilter = {
query: { bool: {} },
};
if ((key && value) || filterField) {
(queryFilter.query.bool as EsBoolQuery).must = isUndefined(filterField)
? { term: { [key ?? '']: value } }
: getExploreQueryFilterMust(filterField);
}
const bucketMapping = isDatabaseHierarchy
? {
[EntityFields.SERVICE_TYPE]: EntityFields.SERVICE,
[EntityFields.SERVICE]: EntityFields.DATABASE_DISPLAY_NAME,
[EntityFields.DATABASE_DISPLAY_NAME]:
EntityFields.DATABASE_SCHEMA_DISPLAY_NAME,
[EntityFields.DATABASE_SCHEMA_DISPLAY_NAME]: EntityFields.ENTITY_TYPE,
}
: {
[EntityFields.SERVICE_TYPE]: EntityFields.SERVICE,
[EntityFields.SERVICE]: EntityFields.ENTITY_TYPE,
};
return {
bucket: bucketMapping[key as DatabaseFields] ?? EntityFields.SERVICE_TYPE,
queryFilter,
};
};
export const getExploreQueryFilterMust = (data: ExploreQuickFilterField[]) => {
const must = [] as Array<QueryFieldInterface>;
@ -281,6 +246,41 @@ export const getExploreQueryFilterMust = (data: ExploreQuickFilterField[]) => {
return must;
};
export const getSubLevelHierarchyKey = (
isDatabaseHierarchy = false,
filterField?: ExploreQuickFilterField[],
key?: EntityFields,
value?: string
) => {
const queryFilter = {
query: { bool: {} },
};
if ((key && value) || filterField) {
(queryFilter.query.bool as EsBoolQuery).must = isUndefined(filterField)
? { term: { [key ?? '']: value } }
: getExploreQueryFilterMust(filterField);
}
const bucketMapping = isDatabaseHierarchy
? {
[EntityFields.SERVICE_TYPE]: EntityFields.SERVICE,
[EntityFields.SERVICE]: EntityFields.DATABASE_DISPLAY_NAME,
[EntityFields.DATABASE_DISPLAY_NAME]:
EntityFields.DATABASE_SCHEMA_DISPLAY_NAME,
[EntityFields.DATABASE_SCHEMA_DISPLAY_NAME]: EntityFields.ENTITY_TYPE,
}
: {
[EntityFields.SERVICE_TYPE]: EntityFields.SERVICE,
[EntityFields.SERVICE]: EntityFields.ENTITY_TYPE,
};
return {
bucket: bucketMapping[key as DatabaseFields] ?? EntityFields.SERVICE_TYPE,
queryFilter,
};
};
export const updateTreeData = (
list: ExploreTreeNode[],
key: React.Key,
@ -360,11 +360,12 @@ export const getAggregationOptions = async (
key: string,
value: string,
filter: string,
isIndependent: boolean
isIndependent: boolean,
deleted = false
) => {
return isIndependent
? postAggregateFieldOptions(index, key, value, filter)
: getAggregateFieldOptions(index, key, value, filter);
: getAggregateFieldOptions(index, key, value, filter, undefined, deleted);
};
export const updateTreeDataWithCounts = (
@ -412,7 +413,7 @@ export const isElasticsearchError = (error: unknown): boolean => {
return false;
}
const data = axiosError.response.data as Record<string, any>;
const data = axiosError.response.data as Record<string, unknown>;
const message = data.message as string;
return (