Skip to content

Commit 5281095

Browse files
committed
fix(advanced-search): move entityStatus to common config with hard-coded list values (#27642)
* fix(advanced-search): move entityStatus to common config with hard-coded list values * lint and unit test fix * addressed PR comment
1 parent 31cfc6d commit 5281095

6 files changed

Lines changed: 345 additions & 20 deletions

File tree

openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/AdvancedSearch.spec.ts

Lines changed: 288 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,14 @@
1111
* limitations under the License.
1212
*/
1313

14+
import { expect } from '@playwright/test';
15+
import { EntityStatus } from '../../../src/generated/entity/data/searchIndex';
1416
import { COMMON_TIER_TAG } from '../../constant/common';
17+
import { DOMAIN_TAGS } from '../../constant/config';
1518
import { SidebarItem } from '../../constant/sidebar';
19+
import { DataProduct } from '../../support/domain/DataProduct';
1620
import { EntityDataClass } from '../../support/entity/EntityDataClass';
21+
import { MlModelClass } from '../../support/entity/MlModelClass';
1722
import { TableClass } from '../../support/entity/TableClass';
1823
import { TopicClass } from '../../support/entity/TopicClass';
1924
import { Glossary } from '../../support/glossary/Glossary';
@@ -22,12 +27,17 @@ import { UserClass } from '../../support/user/UserClass';
2227
import { performAdminLogin } from '../../utils/admin';
2328
import {
2429
FIELDS,
30+
fillRule,
31+
fillStaticListRule,
2532
OPERATOR,
2633
runRuleGroupTests,
2734
runRuleGroupTestsWithNonExistingValue,
35+
selectOption,
36+
showAdvancedSearchDialog,
2837
verifyAllConditions,
2938
} from '../../utils/advancedSearch';
3039
import { redirectToHomePage } from '../../utils/common';
40+
import { waitForAllLoadersToDisappear } from '../../utils/entity';
3141
import { sidebarClick } from '../../utils/sidebar';
3242
import { test } from '../fixtures/pages';
3343

@@ -274,8 +284,8 @@ test.describe('Advanced Search', { tag: ['@advanced-search'] }, () => {
274284
table2.entityResponseData.name,
275285
],
276286
'project.keyword': [
277-
EntityDataClass.dashboardDataModel1.entityResponseData.project,
278-
EntityDataClass.dashboardDataModel2.entityResponseData.project,
287+
EntityDataClass.dashboardDataModel1.entityResponseData.project || '',
288+
EntityDataClass.dashboardDataModel2.entityResponseData.project || '',
279289
],
280290
entityStatus: ['Approved', 'In Review'],
281291
// Some common field value search criteria are causing problems in not equal filter tests
@@ -337,3 +347,279 @@ test.describe('Advanced Search', { tag: ['@advanced-search'] }, () => {
337347
await runRuleGroupTestsWithNonExistingValue(page);
338348
});
339349
});
350+
351+
const ENTITY_STATUSES = Object.values(EntityStatus);
352+
353+
test.describe(
354+
'Advanced Search - Entity Status Filter',
355+
{ tag: [DOMAIN_TAGS.DISCOVERY] },
356+
() => {
357+
type StatusEntry = {
358+
status: EntityStatus;
359+
endpoint: string;
360+
id: () => string;
361+
displayName: () => string;
362+
fqn: () => string;
363+
};
364+
365+
let glossaryForStatus: Glossary;
366+
let glossaryTermApproved: GlossaryTerm;
367+
let mlModelDraft: MlModelClass;
368+
let dataProductInReview: DataProduct;
369+
let statusEntries: StatusEntry[];
370+
371+
test.beforeAll(
372+
'Create mixed entity types with distinct entity statuses',
373+
async ({ browser }) => {
374+
const { apiContext, afterAction } = await performAdminLogin(browser);
375+
376+
glossaryForStatus = new Glossary();
377+
glossaryTermApproved = new GlossaryTerm(glossaryForStatus);
378+
mlModelDraft = new MlModelClass();
379+
dataProductInReview = new DataProduct();
380+
381+
await glossaryForStatus.create(apiContext);
382+
await Promise.all([
383+
glossaryTermApproved.create(apiContext),
384+
mlModelDraft.create(apiContext),
385+
dataProductInReview.create(apiContext),
386+
]);
387+
388+
statusEntries = [
389+
{
390+
status: EntityStatus.Approved,
391+
endpoint: 'glossaryTerms',
392+
id: () => glossaryTermApproved.responseData.id,
393+
displayName: () => glossaryTermApproved.data.displayName,
394+
fqn: () => glossaryTermApproved.responseData.fullyQualifiedName,
395+
},
396+
{
397+
status: EntityStatus.Draft,
398+
endpoint: 'mlmodels',
399+
id: () => mlModelDraft.entityResponseData.id,
400+
displayName: () => mlModelDraft.entity.displayName,
401+
fqn: () => mlModelDraft.entityResponseData.fullyQualifiedName,
402+
},
403+
{
404+
status: EntityStatus.InReview,
405+
endpoint: 'dataProducts',
406+
id: () => dataProductInReview.responseData.id ?? '',
407+
displayName: () => dataProductInReview.data.displayName,
408+
fqn: () =>
409+
dataProductInReview.responseData.fullyQualifiedName ?? '',
410+
},
411+
];
412+
413+
// Patch entityStatus on each entity
414+
await Promise.all(
415+
statusEntries.map(({ id, status, endpoint }) =>
416+
apiContext.patch(`/api/v1/${endpoint}/${id()}`, {
417+
data: [{ op: 'add', path: '/entityStatus', value: status }],
418+
headers: { 'Content-Type': 'application/json-patch+json' },
419+
})
420+
)
421+
);
422+
423+
await afterAction();
424+
}
425+
);
426+
427+
test.beforeEach(async ({ page }) => {
428+
await redirectToHomePage(page);
429+
await sidebarClick(page, SidebarItem.EXPLORE);
430+
});
431+
432+
test('All entity status options are visible in the Status dropdown', async ({
433+
page,
434+
}) => {
435+
await test.step('Open advanced search dialog', async () => {
436+
await showAdvancedSearchDialog(page);
437+
});
438+
439+
await test.step('Select Status field and == operator', async () => {
440+
const ruleLocator = page.locator('.rule').nth(0);
441+
await selectOption(
442+
page,
443+
ruleLocator.locator('.rule--field .ant-select'),
444+
'Status',
445+
true
446+
);
447+
await selectOption(
448+
page,
449+
ruleLocator.locator('.rule--operator .ant-select'),
450+
'=='
451+
);
452+
});
453+
454+
await test.step('Open Status value dropdown and verify all hard-coded options appear', async () => {
455+
const ruleLocator = page.locator('.rule').nth(0);
456+
await ruleLocator.locator('.widget--widget > .ant-select').click();
457+
458+
const dropdown = page
459+
.locator('.ant-select-dropdown')
460+
.filter({ hasText: EntityStatus.Approved })
461+
.last();
462+
463+
await expect(dropdown).toBeVisible();
464+
465+
for (const status of ENTITY_STATUSES) {
466+
await expect(
467+
dropdown
468+
.locator('.ant-select-item-option')
469+
.filter({ hasText: new RegExp(`^${status}$`, 'i') })
470+
.first()
471+
).toBeVisible();
472+
}
473+
});
474+
});
475+
476+
test('Filtering by status "==" shows matching entity and hides others across entity types', async ({
477+
page,
478+
}) => {
479+
test.slow();
480+
481+
for (const entry of statusEntries) {
482+
const { status } = entry;
483+
const matchedName = entry.displayName();
484+
485+
await test.step(`Apply Status == "${status}" AND Name == "${matchedName}"`, async () => {
486+
await showAdvancedSearchDialog(page);
487+
488+
await fillStaticListRule(page, {
489+
fieldLabel: 'Status',
490+
condition: '==',
491+
value: status,
492+
ruleIndex: 1,
493+
});
494+
495+
await page.getByTestId('advanced-search-add-rule').nth(1).click();
496+
497+
await fillRule(page, {
498+
condition: '==',
499+
field: { id: 'Display Name', name: 'displayName.keyword' },
500+
searchCriteria: matchedName,
501+
index: 2,
502+
});
503+
504+
const searchRes = page.waitForResponse(
505+
'/api/v1/search/query?*index=dataAsset*'
506+
);
507+
await page.getByTestId('apply-btn').click();
508+
await searchRes;
509+
await waitForAllLoadersToDisappear(page);
510+
});
511+
512+
await test.step('Filter chip shows the applied status', async () => {
513+
await expect(
514+
page.getByTestId('advance-search-filter-container')
515+
).toContainText(`'${status}'`);
516+
});
517+
518+
await test.step(`"${matchedName}" (${entry.endpoint}) is visible`, async () => {
519+
await expect(
520+
page.getByTestId(`table-data-card_${entry.fqn()}`)
521+
).toBeVisible();
522+
});
523+
524+
await test.step('Entities with other statuses are not in results', async () => {
525+
const otherEntries = statusEntries.filter((e) => e.status !== status);
526+
527+
for (const other of otherEntries) {
528+
await expect(
529+
page.getByTestId(`table-data-card_${other.fqn()}`)
530+
).not.toBeVisible();
531+
}
532+
});
533+
534+
await page.getByTestId('clear-filters').click();
535+
}
536+
});
537+
538+
test('Filtering by status "!=" excludes matched entity but shows all other entity types', async ({
539+
page,
540+
}) => {
541+
const targetEntry = statusEntries.find(
542+
(e) => e.status === EntityStatus.Approved
543+
)!;
544+
const approvedName = targetEntry.displayName();
545+
const otherEntries = statusEntries.filter(
546+
(e) => e.status !== EntityStatus.Approved
547+
);
548+
549+
await test.step('Apply Status != "Approved" AND Name == approved entity name', async () => {
550+
await showAdvancedSearchDialog(page);
551+
552+
await fillStaticListRule(page, {
553+
fieldLabel: 'Status',
554+
condition: '!=',
555+
value: EntityStatus.Approved,
556+
ruleIndex: 1,
557+
});
558+
559+
await page.getByTestId('advanced-search-add-rule').nth(1).click();
560+
561+
await fillRule(page, {
562+
condition: '==',
563+
field: { id: 'Display Name', name: 'displayName.keyword' },
564+
searchCriteria: approvedName,
565+
index: 2,
566+
});
567+
568+
const searchRes = page.waitForResponse(
569+
'/api/v1/search/query?*index=dataAsset*'
570+
);
571+
await page.getByTestId('apply-btn').click();
572+
await searchRes;
573+
await waitForAllLoadersToDisappear(page);
574+
});
575+
576+
await test.step('Filter chip reflects the != condition', async () => {
577+
await expect(
578+
page.getByTestId('advance-search-filter-container')
579+
).toContainText(`'${EntityStatus.Approved}'`);
580+
});
581+
582+
await test.step('GlossaryTerm with Approved status is not visible', async () => {
583+
await expect(
584+
page.getByTestId(`table-data-card_${targetEntry.fqn()}`)
585+
).not.toBeVisible();
586+
});
587+
588+
await test.step('Draft and In Review entities appear when searched by their name and non-Approved status', async () => {
589+
for (const entry of otherEntries) {
590+
await page.getByTestId('clear-filters').click();
591+
await showAdvancedSearchDialog(page);
592+
593+
await fillStaticListRule(page, {
594+
fieldLabel: 'Status',
595+
condition: '!=',
596+
value: EntityStatus.Approved,
597+
ruleIndex: 1,
598+
});
599+
600+
await page.getByTestId('advanced-search-add-rule').nth(1).click();
601+
602+
await fillRule(page, {
603+
condition: '==',
604+
field: { id: 'Display Name', name: 'displayName.keyword' },
605+
searchCriteria: entry.displayName(),
606+
index: 2,
607+
});
608+
609+
const searchRes = page.waitForResponse(
610+
'/api/v1/search/query?*index=dataAsset*'
611+
);
612+
await page.getByTestId('apply-btn').click();
613+
await searchRes;
614+
await waitForAllLoadersToDisappear(page);
615+
616+
await expect(
617+
page.getByTestId(`table-data-card_${entry.fqn()}`)
618+
).toBeVisible();
619+
}
620+
});
621+
622+
await page.getByTestId('clear-filters').click();
623+
});
624+
}
625+
);

openmetadata-ui/src/main/resources/ui/playwright/utils/advancedSearch.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,43 @@ export const runRuleGroupTestsWithNonExistingValue = async (page: Page) => {
702702
await expect(dropdownText).not.toContainText('Loading...');
703703
};
704704

705+
// For fields backed by hard-coded listValues (no aggregate API call), options are
706+
// rendered immediately — use selectOption directly instead of fillRule which waits
707+
// for a network response that never comes.
708+
export const fillStaticListRule = async (
709+
page: Page,
710+
{
711+
fieldLabel,
712+
condition,
713+
value,
714+
ruleIndex,
715+
}: {
716+
fieldLabel: string;
717+
condition: string;
718+
value: string;
719+
ruleIndex: number;
720+
}
721+
) => {
722+
const ruleLocator = page.locator('.rule').nth(ruleIndex - 1);
723+
724+
await selectOption(
725+
page,
726+
ruleLocator.locator('.rule--field .ant-select'),
727+
fieldLabel,
728+
true
729+
);
730+
await selectOption(
731+
page,
732+
ruleLocator.locator('.rule--operator .ant-select'),
733+
condition
734+
);
735+
await selectOption(
736+
page,
737+
ruleLocator.locator('.widget--widget > .ant-select'),
738+
value
739+
);
740+
};
741+
705742
export const getFieldsSuggestionSearchText = (
706743
fieldLabel: string,
707744
data: Record<string, string>

openmetadata-ui/src/main/resources/ui/src/constants/AdvancedSearch.constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ export const GLOSSARY_DROPDOWN_ITEMS = [
215215
},
216216
{
217217
label: 'label.status',
218-
key: EntityFields.GLOSSARY_TERM_STATUS,
218+
key: EntityFields.ENTITY_STATUS,
219219
},
220220
];
221221

openmetadata-ui/src/main/resources/ui/src/enums/AdvancedSearch.enum.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export enum EntityFields {
7575
API_COLLECTION = 'apiCollection.displayName.keyword',
7676
CHART = 'charts.displayName.keyword',
7777
TASK = 'tasks.displayName.keyword',
78-
GLOSSARY_TERM_STATUS = 'entityStatus',
78+
ENTITY_STATUS = 'entityStatus',
7979
REQUEST_SCHEMA_FIELD = 'requestSchema.schemaFields.name.keyword',
8080
RESPONSE_SCHEMA_FIELD = 'responseSchema.schemaFields.name.keyword',
8181
SERVICE_NAME = 'service.name.keyword',

0 commit comments

Comments
 (0)