MINOR: supported all_index in search index configuration form (#16571)

* supported all_index in search index configuration form

* allow clear in select widget

* supported tree select for the entities

* playwright test

* added env for the run application status test

* fix beta badge color, color of checkbox and changes as per comments

* minor fix

* fix sonar issue
This commit is contained in:
Ashish Gupta 2024-07-09 14:57:12 +05:30 committed by GitHub
parent 27f4d9799e
commit 15ae2d3cc3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 616 additions and 64 deletions

View File

@ -29,13 +29,14 @@
"dashboardService",
"pipelineService",
"mlmodelService",
"storageService",
"metadataService",
"searchService",
"entityReportData",
"webAnalyticEntityViewReportData",
"webAnalyticUserActivityReportData",
"domain",
"storedProcedure",
"storageService",
"dataProduct",
"testCaseResolutionStatus"
],

View File

@ -43,14 +43,16 @@
"dashboardService",
"pipelineService",
"mlmodelService",
"storageService",
"metadataService",
"searchService",
"entityReportData",
"webAnalyticEntityViewReportData",
"webAnalyticUserActivityReportData",
"domain",
"storedProcedure",
"storageService",
"dataProduct"
"dataProduct",
"testCaseResolutionStatus"
],
"recreateIndex": true,
"batchSize": "100",

View File

@ -0,0 +1,217 @@
/*
* Copyright 2024 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 test, { expect, Page } from '@playwright/test';
import { GlobalSettingOptions } from '../../constant/settings';
import { getApiContext, redirectToHomePage } from '../../utils/common';
import { settingClick } from '../../utils/sidebar';
// use the admin user to login
test.use({ storageState: 'playwright/.auth/admin.json' });
const verifyApplicationTriggerToastData = async (page: Page) => {
await expect(page.getByRole('alert').first()).toHaveText(
/Application triggered successfully/
);
await page.getByLabel('close').first().click();
};
const verifyLastExecutionStatus = async (page: Page) => {
const { apiContext } = await getApiContext(page);
await expect
.poll(
async () => {
const response = await apiContext
.get(
'/api/v1/apps/name/SearchIndexingApplication/status?offset=0&limit=1'
)
.then((res) => res.json());
return response.data[0]?.status;
},
{
// Custom expect message for reporting, optional.
message: 'To get the last run execution status as success',
intervals: [30_000],
timeout: 300_000,
}
)
.toBe('success');
await page.reload();
await page.waitForSelector('[data-testid="app-run-history-table"]');
await expect(page.getByTestId('pipeline-status')).toContainText('Success');
};
const verifyLastExecutionRun = async (page: Page) => {
const response = await page.waitForResponse(
'/api/v1/apps/name/SearchIndexingApplication/status?offset=0&limit=1'
);
expect(response.status()).toBe(200);
const responseData = await response.json();
if (responseData.data.length > 0) {
expect(responseData.data).toHaveLength(1);
if (responseData.data[0].status === 'running') {
// wait for success status
await verifyLastExecutionStatus(page);
} else {
expect(responseData.data[0].status).toBe('success');
}
}
};
test('Search Index Application', async ({ page }) => {
await test.step('Visit Application page', async () => {
await redirectToHomePage(page);
await settingClick(page, GlobalSettingOptions.APPLICATIONS);
});
await test.step('Verify last execution run', async () => {
await page
.locator(
'[data-testid="search-indexing-application-card"] [data-testid="config-btn"]'
)
.click();
await verifyLastExecutionRun(page);
});
await test.step('Edit application', async () => {
await page.click('[data-testid="edit-button"]');
await page.click('[data-testid="cron-type"]');
await page.click('.rc-virtual-list [title="None"]');
const deployResponse = page.waitForResponse('/api/v1/apps/*');
await page.click('.ant-modal-body [data-testid="deploy-button"]');
await deployResponse;
await expect(page.getByRole('alert').first()).toHaveText(
/Schedule saved successfully/
);
await page.getByLabel('close').first().click();
expect(await page.innerText('[data-testid="schedule-type"]')).toContain(
'None'
);
await page.click('[data-testid="configuration"]');
await page.fill('#root\\/batchSize', '0');
await page.getByTitle('chart').getByLabel('close').click();
await page.click(
'[data-testid="select-widget"] > .ant-select-selector > .ant-select-selection-item'
);
await page.click('[data-testid="select-option-JP"]');
const responseAfterSubmit = page.waitForResponse('/api/v1/apps/*');
await page.click('[data-testid="submit-btn"]');
await responseAfterSubmit;
await expect(page.getByRole('alert').first()).toHaveText(
/Configuration saved successfully/
);
await page.getByLabel('close').first().click();
});
await test.step('Uninstall application', async () => {
await page.click('[data-testid="manage-button"]');
await page.click('[data-testid="uninstall-button-title"]');
const deleteRequest = page.waitForResponse(
'/api/v1/apps/name/SearchIndexingApplication?hardDelete=true'
);
await page.click('[data-testid="save-button"]');
await deleteRequest;
await expect(page.getByRole('alert').first()).toHaveText(
/Application uninstalled/
);
await page.getByLabel('close').first().click();
const card1 = page.locator(
'[data-testid="search-indexing-application-card"]'
);
expect(await card1.isVisible()).toBe(false);
});
await test.step('Install application', async () => {
await page.click('[data-testid="add-application"]');
// Verify response status code
const getMarketPlaceResponse = await page.waitForResponse(
'/api/v1/apps/marketplace?limit=*'
);
expect(getMarketPlaceResponse.status()).toBe(200);
await page.click(
'[data-testid="search-indexing-application-card"] [data-testid="config-btn"]'
);
await page.click('[data-testid="install-application"]');
await page.click('[data-testid="save-button"]');
await page.click('[data-testid="submit-btn"]');
await page.click('[data-testid="cron-type"]');
await page.click('.rc-virtual-list [title="None"]');
expect(await page.innerText('[data-testid="cron-type"]')).toContain('None');
const installApplicationResponse = page.waitForResponse('api/v1/apps');
await page.click('[data-testid="deploy-button"]');
await installApplicationResponse;
await expect(page.getByRole('alert').first()).toHaveText(
/Application installed successfully/
);
await page.getByLabel('close').first().click();
const card = page.locator(
'[data-testid="search-indexing-application-card"]'
);
expect(await card.isVisible()).toBe(true);
});
if (process.env.isOss) {
await test.step('Run application', async () => {
test.slow(true); // Test time shouldn't exceed while re-fetching the history API.
await page.click(
'[data-testid="search-indexing-application-card"] [data-testid="config-btn"]'
);
const triggerPipelineResponse = page.waitForResponse(
'/api/v1/apps/trigger/SearchIndexingApplication'
);
await page.click('[data-testid="run-now-button"]');
await triggerPipelineResponse;
await verifyApplicationTriggerToastData(page);
await page.reload();
await verifyLastExecutionRun(page);
});
}
});

View File

@ -12,8 +12,8 @@
*/
import { APIRequestContext, expect, Page } from '@playwright/test';
import {
ENTITY_PATH,
EntityTypeEndpoint,
ENTITY_PATH,
} from '../support/entity/Entity.interface';
import { UserClass } from '../support/user/UserClass';
import { uuid } from './common';

View File

@ -28,7 +28,6 @@ import { DatabaseServiceType } from '../../../../../generated/entity/data/databa
import { MetadataServiceType } from '../../../../../generated/entity/services/metadataService';
import { MlModelServiceType } from '../../../../../generated/entity/services/mlmodelService';
import { PipelineServiceType } from '../../../../../generated/entity/services/pipelineService';
import { useApplicationStore } from '../../../../../hooks/useApplicationStore';
import { errorMsg, getServiceLogo } from '../../../../../utils/CommonUtils';
import ServiceUtilClassBase from '../../../../../utils/ServiceUtilClassBase';
import Searchbar from '../../../../common/SearchBarComponent/SearchBar.component';
@ -44,7 +43,6 @@ const SelectServiceType = ({
onCancel,
onNext,
}: SelectServiceTypeProps) => {
const { theme } = useApplicationStore();
const { t } = useTranslation();
const [category, setCategory] = useState('');
const [connectorSearchTerm, setConnectorSearchTerm] = useState('');
@ -146,11 +144,7 @@ const SelectServiceType = ({
{BETA_SERVICES.includes(
type as DatabaseServiceType | PipelineServiceType
) ? (
<Badge
className="service-beta-tag"
color={theme.primaryColor}
count={t('label.beta')}
/>
<Badge className="service-beta-tag" count={t('label.beta')} />
) : null}
</p>
</Button>

View File

@ -22,32 +22,50 @@ import {
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { MOCK_SELECT_WIDGET } from '../../../../../mocks/SelectWidget.mock';
import {
MOCK_SELECT_WIDGET,
MOCK_TREE_SELECT_WIDGET,
} from '../../../../../mocks/SelectWidget.mock';
import SelectWidget from './SelectWidget';
jest.mock('./TreeSelectWidget', () =>
jest.fn().mockImplementation(() => <p>TreeSelectWidget</p>)
);
const mockOnFocus = jest.fn();
const mockOnBlur = jest.fn();
const mockOnChange = jest.fn();
const mockProps: WidgetProps = {
const mockBaseProps = {
onFocus: mockOnFocus,
onBlur: mockOnBlur,
onChange: mockOnChange,
registry: {} as Registry,
};
const mockSelectProps: WidgetProps = {
...mockBaseProps,
...MOCK_SELECT_WIDGET,
};
const mockTreeSelectProps: WidgetProps = {
...mockBaseProps,
...MOCK_TREE_SELECT_WIDGET,
};
describe('Test SelectWidget Component', () => {
it('Should render select component', async () => {
render(<SelectWidget {...mockProps} />);
render(<SelectWidget {...mockSelectProps} />);
const selectInput = screen.getByTestId('select-widget');
const treeSelectWidget = screen.queryByText('TreeSelectWidget');
expect(selectInput).toBeInTheDocument();
expect(treeSelectWidget).not.toBeInTheDocument();
});
it('Should be disabled', async () => {
render(<SelectWidget {...mockProps} disabled />);
render(<SelectWidget {...mockSelectProps} disabled />);
const selectInput = await findByRole(
screen.getByTestId('select-widget'),
@ -58,7 +76,7 @@ describe('Test SelectWidget Component', () => {
});
it('Should call onFocus', async () => {
render(<SelectWidget {...mockProps} />);
render(<SelectWidget {...mockSelectProps} />);
const selectInput = screen.getByTestId('select-widget');
@ -68,7 +86,7 @@ describe('Test SelectWidget Component', () => {
});
it('Should call onBlur', async () => {
render(<SelectWidget {...mockProps} />);
render(<SelectWidget {...mockSelectProps} />);
const selectInput = screen.getByTestId('select-widget');
@ -78,7 +96,7 @@ describe('Test SelectWidget Component', () => {
});
it('Should call onChange', async () => {
render(<SelectWidget {...mockProps} />);
render(<SelectWidget {...mockSelectProps} />);
const selectInput = await findByRole(
screen.getByTestId('select-widget'),
@ -97,4 +115,14 @@ describe('Test SelectWidget Component', () => {
expect(mockOnChange).toHaveBeenCalledTimes(1);
});
it('Should render TreeSelectWidget component if uiFieldType is treeSelect', async () => {
render(<SelectWidget {...mockTreeSelectProps} />);
const selectWidget = screen.queryByTestId('select-widget');
const treeSelectWidget = screen.getByText('TreeSelectWidget');
expect(treeSelectWidget).toBeInTheDocument();
expect(selectWidget).not.toBeInTheDocument();
});
});

View File

@ -14,15 +14,18 @@ import { WidgetProps } from '@rjsf/utils';
import { Select } from 'antd';
import { capitalize } from 'lodash';
import React, { FC } from 'react';
import TreeSelectWidget from './TreeSelectWidget';
const SelectWidget: FC<WidgetProps> = (props) => {
if (props.schema.uiFieldType === 'treeSelect') {
return <TreeSelectWidget {...props} />;
}
const { onFocus, onBlur, onChange, ...rest } = props;
const SelectWidget: FC<WidgetProps> = ({
onFocus,
onBlur,
onChange,
...rest
}) => {
return (
<Select
allowClear
autoFocus={rest.autofocus}
className="d-block w-full"
data-testid="select-widget"

View File

@ -0,0 +1,100 @@
/*
* Copyright 2024 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 { Registry } from '@rjsf/utils';
import {
act,
findByRole,
fireEvent,
render,
screen,
waitForElement,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { MOCK_TREE_SELECT_WIDGET } from '../../../../../mocks/SelectWidget.mock';
import TreeSelectWidget from './TreeSelectWidget';
const mockOnFocus = jest.fn();
const mockOnBlur = jest.fn();
const mockOnChange = jest.fn();
const mockProps = {
onFocus: mockOnFocus,
onBlur: mockOnBlur,
onChange: mockOnChange,
registry: {} as Registry,
...MOCK_TREE_SELECT_WIDGET,
};
describe('Test TreeSelectWidget Component', () => {
it('Should render tree select component', async () => {
render(<TreeSelectWidget {...mockProps} />);
const treeSelectWidget = screen.getByTestId('tree-select-widget');
expect(treeSelectWidget).toBeInTheDocument();
});
it('Should be disabled', async () => {
render(<TreeSelectWidget {...mockProps} disabled />);
const treeSelectInput = await findByRole(
screen.getByTestId('tree-select-widget'),
'combobox'
);
expect(treeSelectInput).toBeDisabled();
});
it('Should call onFocus', async () => {
render(<TreeSelectWidget {...mockProps} />);
const treeSelectInput = screen.getByTestId('tree-select-widget');
fireEvent.focus(treeSelectInput);
expect(mockOnFocus).toHaveBeenCalled();
});
it('Should call onBlur', async () => {
render(<TreeSelectWidget {...mockProps} />);
const treeSelectInput = screen.getByTestId('tree-select-widget');
fireEvent.blur(treeSelectInput);
expect(mockOnBlur).toHaveBeenCalled();
});
it('Should call onChange', async () => {
render(<TreeSelectWidget {...mockProps} />);
const treeSelectInput = await findByRole(
screen.getByTestId('tree-select-widget'),
'combobox'
);
await act(async () => {
userEvent.click(treeSelectInput);
});
await waitForElement(() => screen.getAllByText('Table'));
await act(async () => {
fireEvent.click(screen.getAllByText('Table')[1]);
});
expect(mockOnChange).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,72 @@
/*
* Copyright 2024 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 Icon from '@ant-design/icons/lib/components/Icon';
import { WidgetProps } from '@rjsf/utils';
import { TreeSelect } from 'antd';
import { startCase } from 'lodash';
import React, { FC, useMemo } from 'react';
import { ReactComponent as ArrowIcon } from '../../../../../assets/svg/ic-arrow-down.svg';
import { TEXT_BODY_COLOR } from '../../../../../constants/constants';
const TreeSelectWidget: FC<WidgetProps> = ({
onFocus,
onBlur,
onChange,
...rest
}) => {
const treeData = useMemo(
() => [
{
title: 'All',
value: 'all',
key: 'all',
children: rest.options.enumOptions?.map((node) => ({
title: startCase(node.label),
value: node.value,
key: node.value,
})),
},
],
[rest.options.enumOptions]
);
return (
<TreeSelect
allowClear
multiple
showSearch
treeCheckable
treeDefaultExpandAll
data-testid="tree-select-widget"
disabled={rest.disabled}
filterTreeNode={false}
style={{
width: '100%',
}}
switcherIcon={
<Icon
component={ArrowIcon}
data-testid="expand-icon"
style={{ fontSize: '10px', color: TEXT_BODY_COLOR }}
/>
}
treeData={treeData}
onBlur={() => onBlur(rest.id, rest.value)}
onChange={onChange}
onFocus={() => onFocus(rest.id, rest.value)}
{...rest}
/>
);
};
export default TreeSelectWidget;

View File

@ -39,3 +39,172 @@ export const MOCK_SELECT_WIDGET = {
uiSchema: {},
value: 'JP',
};
export const MOCK_TREE_SELECT_WIDGET = {
autofocus: false,
disabled: false,
formContext: { handleFocus: undefined },
hideError: undefined,
hideLabel: false,
id: 'root/entities',
label: 'Entities',
multiple: true,
name: 'entities',
readonly: false,
placeholder: '',
rawErrors: undefined,
options: {
enumOptions: [
{
label: 'table',
value: 'table',
},
{
label: 'dashboard',
value: 'dashboard',
},
{
label: 'topic',
value: 'topic',
},
{
label: 'pipeline',
value: 'pipeline',
},
{
label: 'ingestionPipeline',
value: 'ingestionPipeline',
},
{
label: 'searchIndex',
value: 'searchIndex',
},
{
label: 'user',
value: 'user',
},
{
label: 'team',
value: 'team',
},
{
label: 'glossary',
value: 'glossary',
},
{
label: 'glossaryTerm',
value: 'glossaryTerm',
},
{
label: 'mlmodel',
value: 'mlmodel',
},
{
label: 'tag',
value: 'tag',
},
{
label: 'classification',
value: 'classification',
},
{
label: 'query',
value: 'query',
},
{
label: 'container',
value: 'container',
},
{
label: 'database',
value: 'database',
},
{
label: 'databaseSchema',
value: 'databaseSchema',
},
{
label: 'testCase',
value: 'testCase',
},
{
label: 'testSuite',
value: 'testSuite',
},
{
label: 'chart',
value: 'chart',
},
{
label: 'dashboardDataModel',
value: 'dashboardDataModel',
},
{
label: 'databaseService',
value: 'databaseService',
},
{
label: 'messagingService',
value: 'messagingService',
},
{
label: 'dashboardService',
value: 'dashboardService',
},
{
label: 'pipelineService',
value: 'pipelineService',
},
{
label: 'mlmodelService',
value: 'mlmodelService',
},
{
label: 'storageService',
value: 'storageService',
},
{
label: 'metadataService',
value: 'metadataService',
},
{
label: 'searchService',
value: 'searchService',
},
{
label: 'entityReportData',
value: 'entityReportData',
},
{
label: 'webAnalyticEntityViewReportData',
value: 'webAnalyticEntityViewReportData',
},
{
label: 'webAnalyticUserActivityReportData',
value: 'webAnalyticUserActivityReportData',
},
{
label: 'domain',
value: 'domain',
},
{
label: 'storedProcedure',
value: 'storedProcedure',
},
{
label: 'dataProduct',
value: 'dataProduct',
},
{
label: 'testCaseResolutionStatus',
value: 'testCaseResolutionStatus',
},
],
},
schema: {
title: 'Entities',
description: 'List of entities that you need to reindex',
uiFieldType: 'treeSelect',
},
value: ['all'],
};

View File

@ -138,4 +138,6 @@
.rjsf .checkbox input[type='checkbox'] {
margin-right: 4px;
cursor: pointer;
accent-color: @primary-color;
}

View File

@ -55,44 +55,8 @@
"testCaseResolutionStatus"
]
},
"default": [
"table",
"dashboard",
"topic",
"pipeline",
"searchIndex",
"user",
"team",
"glossary",
"glossaryTerm",
"mlmodel",
"tag",
"classification",
"query",
"container",
"database",
"databaseSchema",
"testCase",
"testSuite",
"chart",
"dashboardDataModel",
"databaseService",
"messagingService",
"dashboardService",
"pipelineService",
"mlmodelService",
"storageService",
"metadataService",
"searchService",
"entityReportData",
"webAnalyticEntityViewReportData",
"webAnalyticUserActivityReportData",
"domain",
"storedProcedure",
"dataProduct",
"ingestionPipeline",
"testCaseResolutionStatus"
],
"default": ["all"],
"uiFieldType": "treeSelect",
"uniqueItems": true
},
"recreateIndex": {