Skip to content

Commit 20cb1b0

Browse files
authored
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 b582ecc commit 20cb1b0

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

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

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

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

695+
// For fields backed by hard-coded listValues (no aggregate API call), options are
696+
// rendered immediately — use selectOption directly instead of fillRule which waits
697+
// for a network response that never comes.
698+
export const fillStaticListRule = async (
699+
page: Page,
700+
{
701+
fieldLabel,
702+
condition,
703+
value,
704+
ruleIndex,
705+
}: {
706+
fieldLabel: string;
707+
condition: string;
708+
value: string;
709+
ruleIndex: number;
710+
}
711+
) => {
712+
const ruleLocator = page.locator('.rule').nth(ruleIndex - 1);
713+
714+
await selectOption(
715+
page,
716+
ruleLocator.locator('.rule--field .ant-select'),
717+
fieldLabel,
718+
true
719+
);
720+
await selectOption(
721+
page,
722+
ruleLocator.locator('.rule--operator .ant-select'),
723+
condition
724+
);
725+
await selectOption(
726+
page,
727+
ruleLocator.locator('.widget--widget > .ant-select'),
728+
value
729+
);
730+
};
731+
695732
export const getFieldsSuggestionSearchText = (
696733
fieldLabel: string,
697734
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)