fix(customHomePage): bring changes for query builder from Saas (#14420)

Co-authored-by: Chris Collins <chriscollins3456@gmail.com>
This commit is contained in:
v-tarasevich-blitz-brain 2025-08-27 20:26:09 +03:00 committed by GitHub
parent c2f9fc812c
commit b1d96ab3bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 227 additions and 70 deletions

View File

@ -4,7 +4,8 @@ import LogicalFiltersBuilder from '@app/sharedV2/queryBuilder/LogicalFiltersBuil
import { LogicalOperatorType, LogicalPredicate } from '@app/sharedV2/queryBuilder/builder/types';
import { properties } from '@app/sharedV2/queryBuilder/properties';
const EMPTY_FILTER = {
const EMPTY_FILTER: LogicalPredicate = {
type: 'logical',
operator: LogicalOperatorType.AND,
operands: [],
};

View File

@ -13,7 +13,8 @@ import LogicalFiltersBuilder from '@app/sharedV2/queryBuilder/LogicalFiltersBuil
import { LogicalOperatorType, LogicalPredicate } from '@app/sharedV2/queryBuilder/builder/types';
import { properties } from '@app/sharedV2/queryBuilder/properties';
const EMPTY_FILTER = {
const EMPTY_FILTER: LogicalPredicate = {
type: 'logical',
operator: LogicalOperatorType.AND,
operands: [],
};

View File

@ -38,25 +38,28 @@ const Condition = ({ selectedPredicate, onDeletePredicate, onChangePredicate, pr
const valueOptions = (property && selectedPredicate && getValueOptions(property, selectedPredicate)) || undefined;
const handlePropertyChange = (propertyId?: string) => {
const newPredicate = {
const newPredicate: PropertyPredicate = {
type: 'property',
property: propertyId,
};
onChangePredicate(newPredicate);
};
const handleOperatorChange = (operatorId: string) => {
const newPredicate = {
const newPredicate: PropertyPredicate = {
...selectedPredicate,
operator: operatorId,
values: undefined,
type: 'property',
};
onChangePredicate(newPredicate);
};
const handleValuesChange = (values: string[]) => {
const newPredicate = {
const newPredicate: PropertyPredicate = {
...selectedPredicate,
values,
type: 'property',
};
onChangePredicate(newPredicate);
};

View File

@ -2,6 +2,7 @@ import { SelectOption, SimpleSelect } from '@components';
import React, { useMemo } from 'react';
import { Operator } from '@app/sharedV2/queryBuilder/builder/property/types/operators';
import { ConditionElementWithFixedWidth } from '@app/sharedV2/queryBuilder/styledComponents';
interface Props {
selectedOperator?: string;
@ -21,16 +22,18 @@ const OperatorSelect = ({ selectedOperator, operators, onChangeOperator }: Props
);
return (
<SimpleSelect
options={options}
placeholder="Select an operator..."
onUpdate={(val) => onChangeOperator(val[0])}
values={selectedOperator ? [selectedOperator.toLowerCase()] : []}
isDisabled={!operators}
data-testid="condition-operator-select"
width="full"
showClear={false}
/>
<ConditionElementWithFixedWidth>
<SimpleSelect
options={options}
placeholder="Select an operator..."
onUpdate={(val) => onChangeOperator(val[0])}
values={selectedOperator ? [selectedOperator.toLowerCase()] : []}
isDisabled={!operators}
data-testid="condition-operator-select"
width="full"
showClear={false}
/>
</ConditionElementWithFixedWidth>
);
};

View File

@ -2,6 +2,7 @@ import { SelectOption, SimpleSelect } from '@components';
import React, { useMemo } from 'react';
import { Property } from '@app/sharedV2/queryBuilder/builder/property/types/properties';
import { ConditionElementWithFixedWidth } from '@app/sharedV2/queryBuilder/styledComponents';
interface Props {
selectedProperty?: string;
@ -21,15 +22,17 @@ const PropertySelect = ({ selectedProperty, properties, onChangeProperty }: Prop
);
return (
<SimpleSelect
options={options}
onUpdate={(val) => onChangeProperty(val[0])}
values={selectedProperty ? [selectedProperty] : []}
placeholder="Select a property"
data-testid="condition-select"
width="full"
showClear={false}
/>
<ConditionElementWithFixedWidth>
<SimpleSelect
options={options}
onUpdate={(val) => onChangeProperty(val[0])}
values={selectedProperty ? [selectedProperty] : []}
placeholder="Select a property"
data-testid="condition-select"
width="full"
showClear={false}
/>
</ConditionElementWithFixedWidth>
);
};

View File

@ -10,7 +10,8 @@ import { convertToLogicalPredicate } from '@app/sharedV2/queryBuilder/builder/ut
import { CardIcons, StyledCollapse } from '@app/sharedV2/queryBuilder/styledComponents';
import { Icon } from '@src/alchemy-components';
const EMPTY_PROPERTY_PREDICATE = {
const EMPTY_PROPERTY_PREDICATE: PropertyPredicate = {
type: 'property',
property: undefined,
operator: undefined,
values: undefined,
@ -31,12 +32,12 @@ const QueryBuilder = ({ selectedPredicate, onChangePredicate, properties, depth,
const logicalPredicate = convertToLogicalPredicate(selectedPredicate);
const { operator } = logicalPredicate;
const operands = useMemo(() => {
const operands: (PropertyPredicate | LogicalPredicate)[] = useMemo(() => {
if (logicalPredicate) {
// Filter out undefined values
if (logicalPredicate.operands && logicalPredicate.operands.some((item) => item === undefined)) {
const newOperands = logicalPredicate.operands.filter((item) => item !== undefined);
onChangePredicate({ operator, operands: newOperands });
onChangePredicate({ type: 'logical', operator, operands: newOperands });
return newOperands;
}
return logicalPredicate.operands;
@ -45,25 +46,26 @@ const QueryBuilder = ({ selectedPredicate, onChangePredicate, properties, depth,
}, [logicalPredicate, onChangePredicate, operator]);
const onAddPropertyPredicate = () => {
const newOperands = [...operands, EMPTY_PROPERTY_PREDICATE];
onChangePredicate({ operator, operands: newOperands });
const newOperands: (PropertyPredicate | LogicalPredicate)[] = [...operands, EMPTY_PROPERTY_PREDICATE];
onChangePredicate({ type: 'logical', operator, operands: newOperands });
};
const onAddLogicalPredicate = () => {
const newPredicate = {
const newPredicate: LogicalPredicate = {
type: 'logical',
operator: LogicalOperatorType.AND,
operands: [],
};
const newOperands = [...operands, newPredicate];
onChangePredicate({ operator, operands: newOperands });
onChangePredicate({ type: 'logical', operator, operands: newOperands });
};
const onChangeOperator = (newOperator) => {
onChangePredicate({ operator: newOperator, operands });
onChangePredicate({ type: 'logical', operator: newOperator, operands });
};
const onChangeOperands = (ops) => {
onChangePredicate({ operator, operands: ops });
onChangePredicate({ type: 'logical', operator, operands: ops });
};
const onDeletePredicate = () => {

View File

@ -1,53 +1,63 @@
import { LogicalOperatorType, LogicalPredicate } from '@app/sharedV2/queryBuilder/builder/types';
import { LogicalOperatorType, LogicalPredicate, PropertyPredicate } from '@app/sharedV2/queryBuilder/builder/types';
import {
convertLogicalPredicateToOrFilters,
convertToLogicalPredicate,
isEmptyLogicalPredicate,
isLogicalPredicate,
} from '@app/sharedV2/queryBuilder/builder/utils';
import { AndFilterInput, FilterOperator } from '@types';
describe('utils', () => {
describe('isLogicalPredicate', () => {
it('test is logical predicate', () => {
expect(
isLogicalPredicate({
type: 'logical',
operator: LogicalOperatorType.AND,
operands: [],
}),
} as LogicalPredicate),
).toEqual(true);
expect(
isLogicalPredicate({
type: 'logical',
operator: LogicalOperatorType.OR,
}),
} as LogicalPredicate),
).toEqual(true);
expect(
isLogicalPredicate({
type: 'logical',
operator: LogicalOperatorType.NOT,
}),
} as LogicalPredicate),
).toEqual(true);
});
it('test is not logical predicate', () => {
expect(
isLogicalPredicate({
operator: 'exists',
}),
} as any),
).toEqual(false);
expect(
isLogicalPredicate({
type: 'property',
property: 'dataset.description',
}),
} as PropertyPredicate),
).toEqual(false);
});
});
const BASIC_AND_LOGICAL_PREDICATE = {
const BASIC_AND_LOGICAL_PREDICATE: LogicalPredicate = {
type: 'logical',
operator: LogicalOperatorType.AND,
operands: [
{
type: 'property',
property: 'test',
operator: 'equals',
values: ['dataset1'],
},
{
type: 'property',
property: 'test2',
operator: 'equals',
values: ['dataset2'],
@ -55,24 +65,27 @@ describe('utils', () => {
],
};
const BASIC_AND_OR_FILTERS = [
const BASIC_AND_OR_FILTERS: AndFilterInput[] = [
{
and: [
{ field: 'test', condition: 'EQUAL', values: ['dataset1'] },
{ field: 'test2', condition: 'EQUAL', values: ['dataset2'] },
{ field: 'test', condition: FilterOperator.Equal, values: ['dataset1'] },
{ field: 'test2', condition: FilterOperator.Equal, values: ['dataset2'] },
],
},
];
const BASIC_OR_LOGICAL_PREDICATE = {
const BASIC_OR_LOGICAL_PREDICATE: LogicalPredicate = {
type: 'logical',
operator: LogicalOperatorType.OR,
operands: [
{
type: 'property',
property: 'test',
operator: 'equals',
values: ['dataset1'],
},
{
type: 'property',
property: 'test2',
operator: 'equals',
values: ['dataset2'],
@ -80,24 +93,27 @@ describe('utils', () => {
],
};
const BASIC_OR_OR_FILTERS = [
const BASIC_OR_OR_FILTERS: AndFilterInput[] = [
{
and: [{ field: 'test', condition: 'EQUAL', values: ['dataset1'] }],
and: [{ field: 'test', condition: FilterOperator.Equal, values: ['dataset1'] }],
},
{
and: [{ field: 'test2', condition: 'EQUAL', values: ['dataset2'] }],
and: [{ field: 'test2', condition: FilterOperator.Equal, values: ['dataset2'] }],
},
];
const BASIC_NOT_LOGICAL_PREDICATE = {
const BASIC_NOT_LOGICAL_PREDICATE: LogicalPredicate = {
type: 'logical',
operator: LogicalOperatorType.NOT,
operands: [
{
type: 'property',
property: 'test',
operator: 'equals',
values: ['dataset1'],
},
{
type: 'property',
property: 'test2',
operator: 'equals',
values: ['dataset2'],
@ -105,32 +121,37 @@ describe('utils', () => {
],
};
const BASIC_NOT_OR_FILTERS = [
const BASIC_NOT_OR_FILTERS: AndFilterInput[] = [
{
and: [
{ field: 'test', condition: 'EQUAL', values: ['dataset1'], negated: true },
{ field: 'test2', condition: 'EQUAL', values: ['dataset2'], negated: true },
{ field: 'test', condition: FilterOperator.Equal, values: ['dataset1'], negated: true },
{ field: 'test2', condition: FilterOperator.Equal, values: ['dataset2'], negated: true },
],
},
];
const NESTED_LOGICAL_PREDICATE = {
const NESTED_LOGICAL_PREDICATE: LogicalPredicate = {
type: 'logical',
operator: LogicalOperatorType.OR,
operands: [
{
type: 'property',
property: 'test1',
operator: 'equals',
values: ['dataset1'],
},
{
type: 'logical',
operator: LogicalOperatorType.AND,
operands: [
{
type: 'property',
property: 'test2',
operator: 'equals',
values: ['dataset2'],
},
{
type: 'property',
property: 'test3',
operator: 'equals',
values: ['dataset3'],
@ -138,22 +159,27 @@ describe('utils', () => {
],
},
{
type: 'logical',
operator: LogicalOperatorType.OR,
operands: [
{
type: 'property',
property: 'test4',
operator: 'equals',
values: ['dataset4'],
},
{
type: 'logical',
operator: LogicalOperatorType.NOT,
operands: [
{
type: 'property',
property: 'test5',
operator: 'equals',
values: ['dataset5'],
},
{
type: 'property',
property: 'test6',
operator: 'equals',
values: ['dataset6'],
@ -165,36 +191,39 @@ describe('utils', () => {
],
};
const NESTED_OR_FILTERS = [
const NESTED_OR_FILTERS: AndFilterInput[] = [
{
and: [{ field: 'test1', condition: 'EQUAL', values: ['dataset1'] }],
and: [{ field: 'test1', condition: FilterOperator.Equal, values: ['dataset1'] }],
},
{
and: [
{ field: 'test2', condition: 'EQUAL', values: ['dataset2'] },
{ field: 'test3', condition: 'EQUAL', values: ['dataset3'] },
{ field: 'test2', condition: FilterOperator.Equal, values: ['dataset2'] },
{ field: 'test3', condition: FilterOperator.Equal, values: ['dataset3'] },
],
},
{
and: [{ field: 'test4', condition: 'EQUAL', values: ['dataset4'] }],
and: [{ field: 'test4', condition: FilterOperator.Equal, values: ['dataset4'] }],
},
{
and: [
{ field: 'test5', condition: 'EQUAL', values: ['dataset5'], negated: true },
{ field: 'test6', condition: 'EQUAL', values: ['dataset6'], negated: true },
{ field: 'test5', condition: FilterOperator.Equal, values: ['dataset5'], negated: true },
{ field: 'test6', condition: FilterOperator.Equal, values: ['dataset6'], negated: true },
],
},
];
const EMPTY_PROPERTY_PREDICATE = {
const EMPTY_PROPERTY_PREDICATE: LogicalPredicate = {
type: 'logical',
operator: LogicalOperatorType.AND,
operands: [
{
type: 'property',
property: 'test',
operator: 'equals',
values: ['dataset1'],
},
{
type: 'property',
property: '',
operator: 'equals',
values: ['dataset2'],
@ -202,21 +231,23 @@ describe('utils', () => {
],
};
const EMPTY_PROPERTY_OR_FILTERS = [
const EMPTY_PROPERTY_OR_FILTERS: AndFilterInput[] = [
{
and: [{ field: 'test', condition: 'EQUAL', values: ['dataset1'] }],
and: [{ field: 'test', condition: FilterOperator.Equal, values: ['dataset1'] }],
},
];
const EMPTY_LOGICAL_PREDICATE = {};
const LOGICAL_PREDICATE_WITH_EMPTY_OPERANDS = {
const EMPTY_LOGICAL_PREDICATE: LogicalPredicate = {} as any;
const LOGICAL_PREDICATE_WITH_EMPTY_OPERANDS: LogicalPredicate = {
type: 'logical',
operator: LogicalOperatorType.AND,
operands: [],
};
const LOGICAL_PREDICATE_WITH_UNKNOWN_OPERATION = {
const LOGICAL_PREDICATE_WITH_UNKNOWN_OPERATION: LogicalPredicate = {
type: 'logical',
operator: 'UNKNOWN',
};
} as any;
describe('convertLogicalPredicateToOrFilters', () => {
it('convert a basic AND predicate to orFilters', () => {
@ -255,4 +286,113 @@ describe('utils', () => {
expect(isEmptyLogicalPredicate(LOGICAL_PREDICATE_WITH_EMPTY_OPERANDS)).toBeTruthy();
});
});
describe('convertToLogicalPredicate', () => {
const SAMPLE_PROPERTY_PREDICATE: PropertyPredicate = {
type: 'property',
property: 'dataset.description',
operator: 'equals',
values: ['test value'],
};
const SAMPLE_LOGICAL_PREDICATE: LogicalPredicate = {
type: 'logical',
operator: LogicalOperatorType.OR,
operands: [
{
type: 'property',
property: 'name',
operator: 'contains',
values: ['example'],
},
{
type: 'property',
property: 'description',
operator: 'exists',
values: [],
},
],
};
it('should convert PropertyPredicate to LogicalPredicate with AND operator', () => {
const result = convertToLogicalPredicate(SAMPLE_PROPERTY_PREDICATE);
expect(result).toEqual({
type: 'logical',
operator: LogicalOperatorType.AND,
operands: [SAMPLE_PROPERTY_PREDICATE],
});
});
it('should return the same LogicalPredicate when input is already a LogicalPredicate', () => {
const result = convertToLogicalPredicate(SAMPLE_LOGICAL_PREDICATE);
expect(result).toEqual(SAMPLE_LOGICAL_PREDICATE);
expect(result).toBe(SAMPLE_LOGICAL_PREDICATE); // Should be the same reference
});
it('should handle PropertyPredicate with minimal properties', () => {
const minimalPropertyPredicate: PropertyPredicate = {
type: 'property',
property: 'title',
};
const result = convertToLogicalPredicate(minimalPropertyPredicate);
expect(result).toEqual({
type: 'logical',
operator: LogicalOperatorType.AND,
operands: [minimalPropertyPredicate],
});
});
it('should handle PropertyPredicate with empty values array', () => {
const propertyPredicateWithEmptyValues: PropertyPredicate = {
type: 'property',
property: 'status',
operator: 'exists',
values: [],
};
const result = convertToLogicalPredicate(propertyPredicateWithEmptyValues);
expect(result).toEqual({
type: 'logical',
operator: LogicalOperatorType.AND,
operands: [propertyPredicateWithEmptyValues],
});
});
it('should handle complex nested LogicalPredicate', () => {
const complexLogicalPredicate: LogicalPredicate = {
type: 'logical',
operator: LogicalOperatorType.AND,
operands: [
{
type: 'property',
property: 'name',
operator: 'equals',
values: ['test'],
},
{
type: 'logical',
operator: LogicalOperatorType.OR,
operands: [
{
type: 'property',
property: 'type',
operator: 'in',
values: ['dataset', 'dashboard'],
},
],
},
],
};
const result = convertToLogicalPredicate(complexLogicalPredicate);
expect(result).toEqual(complexLogicalPredicate);
expect(result).toBe(complexLogicalPredicate); // Should be the same reference
});
});
});

View File

@ -20,6 +20,7 @@ export enum LogicalOperatorType {
* Represents a single compound logical predicate type (AND, OR, or NOT).
*/
export type LogicalPredicate = {
type: 'logical';
operator: LogicalOperatorType;
operands: (PropertyPredicate | LogicalPredicate)[];
};
@ -29,6 +30,7 @@ export type LogicalPredicate = {
* property, an operator, and a set of values.
*/
export interface PropertyPredicate {
type: 'property';
property?: string; // -> the identifier of the property. e.g. 'name'
operator?: string; // -> the identifier of the operator. e.g. 'exists'
values?: string[]; // -> the value to compare against. e.g. ['something']

View File

@ -15,7 +15,7 @@ export const getOperatorDisplayName = (operator: LogicalOperatorType) => {
* Returns true if the predicate is a logical predicate, as opposed
* to a property predicate.
*/
export const isLogicalPredicate = (predicate: LogicalPredicate | PropertyPredicate) => {
export const isLogicalPredicate = (predicate: LogicalPredicate | PropertyPredicate): predicate is LogicalPredicate => {
const logicalPredicate = predicate as LogicalPredicate;
return (logicalPredicate.operator && LOGICAL_OPERATORS.has(logicalPredicate.operator)) || false;
};
@ -102,6 +102,7 @@ export const convertToLogicalPredicate = (predicate: LogicalPredicate | Property
// If we have a property predicate, simply convert to a basic logical predicate.
if (!isLogicalPredicate(predicate)) {
return {
type: 'logical',
operator: LogicalOperatorType.AND,
operands: [predicate],
};

View File

@ -12,6 +12,10 @@ export const ConditionContainer = styled.div<{ depth: number }>`
padding-left: ${(props) => props.depth * 20 + 50 + 8}px;
`;
export const ConditionElementWithFixedWidth = styled.div`
width: 175px;
`;
export const SelectContainer = styled.div`
display: flex;
gap: 16px;
@ -92,8 +96,6 @@ export const CardIcons = styled.div`
display: flex;
justify-content: end;
gap: 12px;
min-width: 28px;
min-height: 28px;
div {
border: 1px solid ${REDESIGN_COLORS.SILVER_GREY};

View File

@ -129,7 +129,6 @@ export const EntitySearchValueInput = ({ selectedUrns, entityTypes, mode, label,
</StyledOptionWrapper>
);
}
console.log('>>> no cache hit for ', option.value);
return <span>{option.label}</span>;
},
[entityCache],