Appconfig fixes (#20960)

* fix initial config for json logic query builder

* show alert for application

* hide hidden fields from doc
This commit is contained in:
Karan Hotchandani 2025-04-29 10:24:04 +05:30 committed by GitHub
parent b3d7f97590
commit dbebaff32b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 395 additions and 104 deletions

View File

@ -17,6 +17,12 @@ const path = require('path');
const SCHEMA_DIR = path.join(__dirname, './src/utils/ApplicationSchemas'); const SCHEMA_DIR = path.join(__dirname, './src/utils/ApplicationSchemas');
const DOCS_DIR = path.join(__dirname, './public/locales/en-US/Applications'); const DOCS_DIR = path.join(__dirname, './public/locales/en-US/Applications');
const IGNORE_FIELDS = [
'moduleConfiguration.dataAssets.serviceFilter',
'entityLink',
'type',
];
const resolveRef = (schema, ref) => { const resolveRef = (schema, ref) => {
const path = ref.split('/').slice(1); const path = ref.split('/').slice(1);
let current = schema; let current = schema;
@ -27,6 +33,12 @@ const resolveRef = (schema, ref) => {
}; };
const processProperty = (key, prop, schema) => { const processProperty = (key, prop, schema) => {
// Skip if the key is in IGNORE_FIELDS
const currentKey = key.split('.').pop();
if (IGNORE_FIELDS.includes(currentKey)) {
return '';
}
let markdown = `$$section\n`; let markdown = `$$section\n`;
markdown += `### ${prop.title || key} $(id="${key}")\n\n`; markdown += `### ${prop.title || key} $(id="${key}")\n\n`;

View File

@ -2,23 +2,9 @@
Configuration for the AutoPilot Application. Configuration for the AutoPilot Application.
$$section
### Application Type $(id="type")
Application Type
$$
$$section $$section
### Active $(id="active") ### Active $(id="active")
Whether the AutoPilot Workflow should be active or not. Whether the AutoPilot Workflow should be active or not.
$$ $$
$$section
### Service Entity Link $(id="entityLink")
Service Entity Link for which to trigger the application.
$$

View File

@ -2,13 +2,6 @@
This schema defines configuration for the Data Insights Application. This schema defines configuration for the Data Insights Application.
$$section
### Application Type $(id="type")
Application Type
$$
$$section $$section
### batchSize $(id="batchSize") ### batchSize $(id="batchSize")

View File

@ -30,13 +30,13 @@ import {
ValueSource, ValueSource,
} from 'react-awesome-query-builder'; } from 'react-awesome-query-builder';
import { useHistory, useParams } from 'react-router-dom'; import { useHistory, useParams } from 'react-router-dom';
import { emptyJsonTree } from '../../../constants/AdvancedSearch.constants';
import { SearchIndex } from '../../../enums/search.enum'; import { SearchIndex } from '../../../enums/search.enum';
import useCustomLocation from '../../../hooks/useCustomLocation/useCustomLocation'; import useCustomLocation from '../../../hooks/useCustomLocation/useCustomLocation';
import { TabsInfoData } from '../../../pages/ExplorePage/ExplorePage.interface'; import { TabsInfoData } from '../../../pages/ExplorePage/ExplorePage.interface';
import { getAllCustomProperties } from '../../../rest/metadataTypeAPI'; import { getAllCustomProperties } from '../../../rest/metadataTypeAPI';
import advancedSearchClassBase from '../../../utils/AdvancedSearchClassBase'; import advancedSearchClassBase from '../../../utils/AdvancedSearchClassBase';
import { import {
getEmptyJsonTree,
getTierOptions, getTierOptions,
getTreeConfig, getTreeConfig,
} from '../../../utils/AdvancedSearchUtils'; } from '../../../utils/AdvancedSearchUtils';
@ -114,7 +114,7 @@ export const AdvanceSearchProvider = ({
const [initialised, setInitialised] = useState(false); const [initialised, setInitialised] = useState(false);
const defaultTree = useMemo( const defaultTree = useMemo(
() => QbUtils.checkTree(QbUtils.loadTree(emptyJsonTree), config), () => QbUtils.checkTree(QbUtils.loadTree(getEmptyJsonTree()), config),
[] []
); );
@ -197,7 +197,9 @@ export const AdvanceSearchProvider = ({
}; };
const handleReset = useCallback(() => { const handleReset = useCallback(() => {
setTreeInternal(QbUtils.checkTree(QbUtils.loadTree(emptyJsonTree), config)); setTreeInternal(
QbUtils.checkTree(QbUtils.loadTree(getEmptyJsonTree()), config)
);
setQueryFilter(undefined); setQueryFilter(undefined);
setSQLQuery(''); setSQLQuery('');
}, [config]); }, [config]);

View File

@ -35,19 +35,23 @@ import {
Query, Query,
Utils as QbUtils, Utils as QbUtils,
} from 'react-awesome-query-builder'; } from 'react-awesome-query-builder';
import {
EntityFields,
EntityReferenceFields,
} from '../../../../../../enums/AdvancedSearch.enum';
import { EntityType } from '../../../../../../enums/entity.enum'; import { EntityType } from '../../../../../../enums/entity.enum';
import { SearchIndex } from '../../../../../../enums/search.enum'; import { SearchIndex } from '../../../../../../enums/search.enum';
import { import { QueryFilterInterface } from '../../../../../../pages/ExplorePage/ExplorePage.interface';
EsBoolQuery,
QueryFieldInterface,
} from '../../../../../../pages/ExplorePage/ExplorePage.interface';
import { searchQuery } from '../../../../../../rest/searchAPI'; import { searchQuery } from '../../../../../../rest/searchAPI';
import { getEmptyJsonTree } from '../../../../../../utils/AdvancedSearchUtils';
import { import {
elasticSearchFormat, elasticSearchFormat,
elasticSearchFormatForJSONLogic, elasticSearchFormatForJSONLogic,
} from '../../../../../../utils/QueryBuilderElasticsearchFormatUtils'; } from '../../../../../../utils/QueryBuilderElasticsearchFormatUtils';
import { import {
addEntityTypeFilter,
elasticsearchToJsonLogic, elasticsearchToJsonLogic,
getEntityTypeAggregationFilter,
getJsonTreeFromQueryFilter, getJsonTreeFromQueryFilter,
jsonLogicToElasticsearch, jsonLogicToElasticsearch,
READONLY_SETTINGS, READONLY_SETTINGS,
@ -82,29 +86,46 @@ const QueryBuilderWidget: FC<WidgetProps> = ({
const outputType = schema?.outputType ?? SearchOutputType.ElasticSearch; const outputType = schema?.outputType ?? SearchOutputType.ElasticSearch;
const isSearchIndexUpdatedInContext = searchIndexFromContext === searchIndex; const isSearchIndexUpdatedInContext = searchIndexFromContext === searchIndex;
const [initDone, setInitDone] = useState<boolean>(false); const [initDone, setInitDone] = useState<boolean>(false);
const [queryURL, setQueryURL] = useState<string>('');
const fetchEntityCount = useCallback( const fetchEntityCount = useCallback(
async (queryFilter: Record<string, unknown>) => { async (queryFilter: Record<string, unknown>) => {
const qFilter = getEntityTypeAggregationFilter(
queryFilter as unknown as QueryFilterInterface,
entityType
);
const tree = QbUtils.checkTree(
QbUtils.loadTree(getJsonTreeFromQueryFilter(qFilter) as JsonTree),
config
);
const queryFilterString = !isEmpty(tree)
? Qs.stringify({ queryFilter: JSON.stringify(tree) })
: '';
setQueryURL(`${getExplorePath({})}${queryFilterString}`);
try { try {
setIsCountLoading(true); setIsCountLoading(true);
const res = await searchQuery({ const res = await searchQuery({
query: '', query: '',
pageNumber: 0, pageNumber: 0,
pageSize: 0, pageSize: 0,
queryFilter, queryFilter: qFilter as unknown as Record<string, unknown>,
searchIndex: SearchIndex.ALL, searchIndex: SearchIndex.ALL,
includeDeleted: false, includeDeleted: false,
trackTotalHits: true, trackTotalHits: true,
fetchSource: false, fetchSource: false,
}); });
setSearchResults(res.hits.total.value ?? 0); setSearchResults(res.hits.total.value ?? 0);
} catch (_) { } catch {
// silent fail // silent fail
} finally { } finally {
setIsCountLoading(false); setIsCountLoading(false);
} }
}, },
[] [entityType]
); );
const debouncedFetchEntityCount = useMemo( const debouncedFetchEntityCount = useMemo(
@ -112,14 +133,6 @@ const QueryBuilderWidget: FC<WidgetProps> = ({
[fetchEntityCount] [fetchEntityCount]
); );
const queryURL = useMemo(() => {
const queryFilterString = !isEmpty(treeInternal)
? Qs.stringify({ queryFilter: JSON.stringify(treeInternal) })
: '';
return `${getExplorePath({})}${queryFilterString}`;
}, [treeInternal]);
const showFilteredResourceCount = useMemo( const showFilteredResourceCount = useMemo(
() => () =>
outputType === SearchOutputType.ElasticSearch && outputType === SearchOutputType.ElasticSearch &&
@ -138,31 +151,14 @@ const QueryBuilderWidget: FC<WidgetProps> = ({
query: data, query: data,
}; };
if (data) { if (data) {
if (entityType !== EntityType.ALL) { const qFilterWithEntityType = addEntityTypeFilter(
// Scope the search to the passed entity type qFilter as unknown as QueryFilterInterface,
if ( entityType
Array.isArray( );
((qFilter.query as QueryFieldInterface)?.bool as EsBoolQuery)
?.must debouncedFetchEntityCount(
) qFilterWithEntityType as unknown as Record<string, unknown>
) { );
(
(qFilter.query as QueryFieldInterface)?.bool
?.must as QueryFieldInterface[]
)?.push({
bool: {
must: [
{
term: {
entityType: entityType,
},
},
],
},
});
}
}
debouncedFetchEntityCount(qFilter);
} }
onChange(!isEmpty(data) ? JSON.stringify(qFilter) : ''); onChange(!isEmpty(data) ? JSON.stringify(qFilter) : '');
@ -187,22 +183,22 @@ const QueryBuilderWidget: FC<WidgetProps> = ({
const loadDefaultValueInTree = useCallback(() => { const loadDefaultValueInTree = useCallback(() => {
if (!isEmpty(value)) { if (!isEmpty(value)) {
const parsedValue = JSON.parse(value ?? '{}');
if (outputType === SearchOutputType.ElasticSearch) { if (outputType === SearchOutputType.ElasticSearch) {
const parsedTree = getJsonTreeFromQueryFilter( const parsedTree = getJsonTreeFromQueryFilter(
JSON.parse(value || ''), parsedValue,
config.fields config.fields
) as JsonTree; ) as JsonTree;
if (Object.keys(parsedTree).length > 0) { if (Object.keys(parsedTree).length > 0) {
const tree = QbUtils.checkTree(QbUtils.loadTree(parsedTree), config); const tree = QbUtils.checkTree(QbUtils.loadTree(parsedTree), config);
onTreeUpdate(tree, config); onTreeUpdate(tree, config);
// Fetch count for default value
debouncedFetchEntityCount(parsedValue);
} }
} else { } else {
try { try {
const query = jsonLogicToElasticsearch( const query = jsonLogicToElasticsearch(parsedValue, config.fields);
JSON.parse(value || ''),
config.fields
);
const updatedQ = { const updatedQ = {
query: query, query: query,
}; };
@ -225,6 +221,16 @@ const QueryBuilderWidget: FC<WidgetProps> = ({
console.log(e); console.log(e);
} }
} }
} else {
const emptyJsonTree = getEmptyJsonTree(
outputType === SearchOutputType.JSONLogic
? EntityReferenceFields.OWNERS
: EntityFields.OWNERS
);
onTreeUpdate(
QbUtils.checkTree(QbUtils.loadTree(emptyJsonTree), config),
config
);
} }
setInitDone(true); setInitDone(true);
}, [config, value, outputType]); }, [config, value, outputType]);

View File

@ -12,7 +12,6 @@
*/ */
import { t } from 'i18next'; import { t } from 'i18next';
import { JsonTree, Utils as QbUtils } from 'react-awesome-query-builder';
import { EntityFields } from '../enums/AdvancedSearch.enum'; import { EntityFields } from '../enums/AdvancedSearch.enum';
import { SearchIndex } from '../enums/search.enum'; import { SearchIndex } from '../enums/search.enum';
@ -304,39 +303,6 @@ export const RANGE_FIELD_OPERATORS = ['between', 'not_between'];
export const LIST_VALUE_OPERATORS = ['select_equals', 'select_not_equals']; export const LIST_VALUE_OPERATORS = ['select_equals', 'select_not_equals'];
/**
* Generates a query builder tree with a group containing an empty rule
*/
export const emptyJsonTree: JsonTree = {
id: QbUtils.uuid(),
type: 'group',
properties: {
conjunction: 'AND',
not: false,
},
children1: {
[QbUtils.uuid()]: {
type: 'group',
properties: {
conjunction: 'AND',
not: false,
},
children1: {
[QbUtils.uuid()]: {
type: 'rule',
properties: {
// owner is common field , so setting owner as default field here
field: EntityFields.OWNERS,
operator: null,
value: [],
valueSrc: ['value'],
},
},
},
},
},
};
export const MISC_FIELDS = ['owner.displayName', 'tags.tagFQN']; export const MISC_FIELDS = ['owner.displayName', 'tags.tagFQN'];
export const OWNER_QUICK_FILTER_DEFAULT_OPTIONS_KEY = 'displayName.keyword'; export const OWNER_QUICK_FILTER_DEFAULT_OPTIONS_KEY = 'displayName.keyword';

View File

@ -12,10 +12,12 @@
*/ */
import { SearchDropdownOption } from '../components/SearchDropdown/SearchDropdown.interface'; import { SearchDropdownOption } from '../components/SearchDropdown/SearchDropdown.interface';
import { EntityFields } from '../enums/AdvancedSearch.enum';
import { SearchIndex } from '../enums/search.enum'; import { SearchIndex } from '../enums/search.enum';
import { import {
getChartsOptions, getChartsOptions,
getColumnsOptions, getColumnsOptions,
getEmptyJsonTree,
getOptionsFromAggregationBucket, getOptionsFromAggregationBucket,
getSchemaFieldOptions, getSchemaFieldOptions,
getSearchDropdownLabels, getSearchDropdownLabels,
@ -45,6 +47,13 @@ import {
mockShortOptionsArray, mockShortOptionsArray,
} from './mocks/AdvancedSearchUtils.mock'; } from './mocks/AdvancedSearchUtils.mock';
// Mock QbUtils
jest.mock('react-awesome-query-builder', () => ({
Utils: {
uuid: jest.fn().mockReturnValue('test-uuid'),
},
}));
describe('AdvancedSearchUtils tests', () => { describe('AdvancedSearchUtils tests', () => {
it('Function getSearchDropdownLabels should return menuItems for passed options', () => { it('Function getSearchDropdownLabels should return menuItems for passed options', () => {
const resultMenuItems = getSearchDropdownLabels(mockOptionsArray, true); const resultMenuItems = getSearchDropdownLabels(mockOptionsArray, true);
@ -215,4 +224,55 @@ describe('AdvancedSearchUtils tests', () => {
{ count: 3, key: 'chart', label: 'chart' }, { count: 3, key: 'chart', label: 'chart' },
]); ]);
}); });
describe('getEmptyJsonTree', () => {
it('should return a default JsonTree structure with OWNERS as the default field', () => {
const result = getEmptyJsonTree();
const expected = {
id: 'test-uuid',
type: 'group',
properties: {
conjunction: 'AND',
not: false,
},
children1: {
'test-uuid': {
type: 'group',
properties: {
conjunction: 'AND',
not: false,
},
children1: {
'test-uuid': {
type: 'rule',
properties: {
field: EntityFields.OWNERS,
operator: null,
value: [],
valueSrc: ['value'],
},
},
},
},
},
};
expect(result).toEqual(expected);
});
it('should use the provided field when passed as parameter', () => {
const customField = EntityFields.TAG;
const result = getEmptyJsonTree(customField);
const children1 = result.children1 as Record<
string,
{ children1: Record<string, { properties: { field: string } }> }
>;
expect(
children1['test-uuid'].children1['test-uuid'].properties.field
).toEqual(customField);
});
});
}); });

View File

@ -18,8 +18,10 @@ import { isArray, isEmpty, toLower } from 'lodash';
import React from 'react'; import React from 'react';
import { import {
AsyncFetchListValues, AsyncFetchListValues,
JsonTree,
ListValues, ListValues,
RenderSettings, RenderSettings,
Utils as QbUtils,
} from 'react-awesome-query-builder'; } from 'react-awesome-query-builder';
import { ReactComponent as IconDeleteColored } from '../assets/svg/ic-delete-colored.svg'; import { ReactComponent as IconDeleteColored } from '../assets/svg/ic-delete-colored.svg';
import ProfilePicture from '../components/common/ProfilePicture/ProfilePicture'; import ProfilePicture from '../components/common/ProfilePicture/ProfilePicture';
@ -33,6 +35,7 @@ import {
LINEAGE_DROPDOWN_ITEMS, LINEAGE_DROPDOWN_ITEMS,
} from '../constants/AdvancedSearch.constants'; } from '../constants/AdvancedSearch.constants';
import { NOT_INCLUDE_AGGREGATION_QUICK_FILTER } from '../constants/explore.constants'; import { NOT_INCLUDE_AGGREGATION_QUICK_FILTER } from '../constants/explore.constants';
import { EntityFields } from '../enums/AdvancedSearch.enum';
import { EntityType } from '../enums/entity.enum'; import { EntityType } from '../enums/entity.enum';
import { SearchIndex } from '../enums/search.enum'; import { SearchIndex } from '../enums/search.enum';
import { import {
@ -410,3 +413,36 @@ export const getCustomPropertyAdvanceSearchEnumOptions = (
return acc; return acc;
}, {}); }, {});
}; };
export const getEmptyJsonTree = (
defaultField: string = EntityFields.OWNERS
): JsonTree => {
return {
id: QbUtils.uuid(),
type: 'group',
properties: {
conjunction: 'AND',
not: false,
},
children1: {
[QbUtils.uuid()]: {
type: 'group',
properties: {
conjunction: 'AND',
not: false,
},
children1: {
[QbUtils.uuid()]: {
type: 'rule',
properties: {
field: defaultField,
operator: null,
value: [],
valueSrc: ['value'],
},
},
},
},
},
};
};

View File

@ -11,8 +11,14 @@
* limitations under the License. * limitations under the License.
*/ */
import { Fields } from 'react-awesome-query-builder'; import { Fields } from 'react-awesome-query-builder';
import { QueryFilterInterface } from '../pages/ExplorePage/ExplorePage.interface'; import { EntityType } from '../enums/entity.enum';
import { import {
QueryFieldInterface,
QueryFilterInterface,
} from '../pages/ExplorePage/ExplorePage.interface';
import {
addEntityTypeFilter,
getEntityTypeAggregationFilter,
getJsonTreeFromQueryFilter, getJsonTreeFromQueryFilter,
resolveFieldType, resolveFieldType,
} from './QueryBuilderUtils'; } from './QueryBuilderUtils';
@ -152,3 +158,175 @@ describe('resolveFieldType', () => {
expect(resolveFieldType(undefined, 'name')).toBe(''); expect(resolveFieldType(undefined, 'name')).toBe('');
}); });
}); });
describe('addEntityTypeFilter', () => {
const baseQueryFilter: QueryFilterInterface = {
query: {
bool: {
must: [
{
bool: {
must: [
{
term: {
field1: 'value1',
},
},
],
},
},
],
},
},
};
it('should return the original filter when entityType is ALL', () => {
const result = addEntityTypeFilter({ ...baseQueryFilter }, EntityType.ALL);
expect(result).toEqual(baseQueryFilter);
});
it('should add entity type filter for non-ALL entity types', () => {
const result = addEntityTypeFilter(
{ ...baseQueryFilter },
EntityType.TABLE
);
// Assert the must array exists and has correct length
expect(result.query?.bool?.must).toBeDefined();
const mustArray = result.query?.bool?.must as QueryFieldInterface[];
expect(Array.isArray(mustArray)).toBe(true);
expect(mustArray).toHaveLength(2);
// Assert the entity type filter is added correctly
expect(mustArray[1]).toEqual({
bool: {
must: [
{
term: {
entityType: EntityType.TABLE,
},
},
],
},
});
});
it('should handle undefined must array gracefully', () => {
const queryFilter: QueryFilterInterface = {
query: {
bool: {},
},
};
const result = addEntityTypeFilter(queryFilter, EntityType.TABLE);
expect(result).toEqual(queryFilter);
});
it('should handle empty query gracefully', () => {
const queryFilter = {} as QueryFilterInterface;
const result = addEntityTypeFilter(queryFilter, EntityType.TABLE);
expect(result).toEqual(queryFilter);
});
});
describe('getEntityTypeAggregationFilter', () => {
const baseQueryFilter: QueryFilterInterface = {
query: {
bool: {
must: [
{
bool: {
must: [
{
term: {
field1: 'value1',
},
},
],
},
},
],
},
},
};
it('should add entity type to the first must block', () => {
const result = getEntityTypeAggregationFilter(
{ ...baseQueryFilter },
EntityType.TABLE
);
// Assert the must array exists
expect(result.query?.bool?.must).toBeDefined();
const mustArray = result.query?.bool?.must as QueryFieldInterface[];
expect(Array.isArray(mustArray)).toBe(true);
expect(mustArray.length).toBeGreaterThan(0);
// Get the first must block and assert its structure
const firstMustBlock = mustArray[0];
const mustBlockArray = firstMustBlock.bool?.must as QueryFieldInterface[];
expect(mustBlockArray).toBeDefined();
expect(Array.isArray(mustBlockArray)).toBe(true);
expect(mustBlockArray).toHaveLength(2);
// Assert the entity type filter is added correctly
expect(mustBlockArray[1]).toEqual({
term: {
entityType: EntityType.TABLE,
},
});
});
it('should handle undefined must array in first block gracefully', () => {
const queryFilter: QueryFilterInterface = {
query: {
bool: {
must: [
{
bool: {},
},
],
},
},
};
const result = getEntityTypeAggregationFilter(
queryFilter,
EntityType.TABLE
);
expect(result).toEqual(queryFilter);
});
it('should handle empty must array gracefully', () => {
const queryFilter: QueryFilterInterface = {
query: {
bool: {
must: [],
},
},
};
const result = getEntityTypeAggregationFilter(
queryFilter,
EntityType.TABLE
);
expect(result).toEqual(queryFilter);
});
it('should handle empty query gracefully', () => {
const queryFilter = {} as QueryFilterInterface;
const result = getEntityTypeAggregationFilter(
queryFilter,
EntityType.TABLE
);
expect(result).toEqual(queryFilter);
});
});

View File

@ -21,6 +21,7 @@ import {
RenderSettings, RenderSettings,
} from 'react-awesome-query-builder'; } from 'react-awesome-query-builder';
import { EntityReferenceFields } from '../enums/AdvancedSearch.enum'; import { EntityReferenceFields } from '../enums/AdvancedSearch.enum';
import { EntityType } from '../enums/entity.enum';
import { import {
EsBoolQuery, EsBoolQuery,
EsExistsQuery, EsExistsQuery,
@ -801,3 +802,54 @@ export const jsonLogicToElasticsearch = (
throw new Error('Unsupported JSON Logic format'); throw new Error('Unsupported JSON Logic format');
}; };
/**
* Adds entity type filter to the query filter if entity type is specified
* @param qFilter Query filter to add entity type to
* @param entityType Entity type to filter by
* @returns Updated query filter with entity type
*/
export const addEntityTypeFilter = (
qFilter: QueryFilterInterface,
entityType: string
): QueryFilterInterface => {
if (entityType === EntityType.ALL) {
return qFilter;
}
if (Array.isArray((qFilter.query?.bool as EsBoolQuery)?.must)) {
(qFilter.query?.bool?.must as QueryFieldInterface[])?.push({
bool: {
must: [
{
term: {
entityType: entityType,
},
},
],
},
});
}
return qFilter;
};
export const getEntityTypeAggregationFilter = (
qFilter: QueryFilterInterface,
entityType: string
): QueryFilterInterface => {
if (Array.isArray((qFilter.query?.bool as EsBoolQuery)?.must)) {
const firstMustBlock = (
qFilter.query?.bool?.must as QueryFieldInterface[]
)[0];
if (firstMustBlock?.bool?.must) {
(firstMustBlock.bool.must as QueryFieldInterface[]).push({
term: {
entityType: entityType,
},
});
}
}
return qFilter;
};