mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-02 03:29:03 +00:00
Merge remote-tracking branch 'origin/main' into fix-superset-form-bug
This commit is contained in:
commit
77e16cfb6d
@ -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
|
||||
|
||||
@ -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.")
|
||||
|
||||
@ -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
|
||||
"""
|
||||
)
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -26,7 +26,7 @@ class MlFlowIngestionClass extends ServiceBaseClass {
|
||||
shouldAddDefaultFilters?: boolean;
|
||||
}) {
|
||||
const {
|
||||
shouldTestConnection = true,
|
||||
shouldTestConnection = false,
|
||||
shouldAddIngestion = false,
|
||||
shouldAddDefaultFilters = false,
|
||||
} = extraParams ?? {};
|
||||
|
||||
@ -104,8 +104,6 @@ class ServiceBaseClass {
|
||||
await this.fillConnectionDetails(page);
|
||||
|
||||
if (this.shouldTestConnection) {
|
||||
expect(page.getByTestId('next-button')).not.toBeVisible();
|
||||
|
||||
await testConnection(page);
|
||||
}
|
||||
|
||||
|
||||
@ -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++;
|
||||
}
|
||||
};
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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: (
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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"
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -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} />
|
||||
)}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -23,7 +23,6 @@ export interface TestConnectionProps {
|
||||
serviceName?: string;
|
||||
shouldValidateForm?: boolean;
|
||||
onValidateFormRequiredFields?: () => boolean;
|
||||
onTestConnection?: () => void;
|
||||
hostIp?: string;
|
||||
}
|
||||
|
||||
|
||||
@ -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?.();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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'];
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "לא ריק",
|
||||
|
||||
@ -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ではない",
|
||||
|
||||
@ -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": "널이 아님",
|
||||
|
||||
@ -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": "नॉट नल",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "غیر تهی",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "Не пустой",
|
||||
|
||||
@ -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": "ไม่เป็นค่าว่าง",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "非空",
|
||||
|
||||
@ -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": "非空值",
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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>>(
|
||||
|
||||
@ -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.)
|
||||
}));
|
||||
};
|
||||
|
||||
@ -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 (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user