Fix #3928 UI: Explore page filters should include service name (#4029)

This commit is contained in:
Sachin Chaurasiya 2022-04-11 22:11:47 +05:30 committed by GitHub
parent d6b49f070c
commit 595f2a3b8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 439 additions and 46 deletions

View File

@ -51,6 +51,7 @@ import {
getCurrentIndex,
getCurrentTab,
getQueryParam,
INITIAL_FILTERS,
INITIAL_SORT_FIELD,
INITIAL_SORT_ORDER,
tabsInfo,
@ -96,7 +97,7 @@ const Explore: React.FC<ExploreProps> = ({
const location = useLocation();
const history = useHistory();
const filterObject: FilterObject = {
...{ tags: [], service: [], tier: [], database: [] },
...INITIAL_FILTERS,
...getQueryParam(location.search),
};
const [data, setData] = useState<Array<FormatedTableData>>([]);
@ -295,6 +296,24 @@ const Explore: React.FC<ExploreProps> = ({
sortOrder: sortOrder,
searchIndex: searchIndex,
},
{
queryString: searchText,
from: currentPage,
size: ZERO_SIZE,
filters: getFilterString(filters, ['databaseschema']),
sortField: sortField,
sortOrder: sortOrder,
searchIndex: searchIndex,
},
{
queryString: searchText,
from: currentPage,
size: ZERO_SIZE,
filters: getFilterString(filters, ['servicename']),
sortField: sortField,
sortOrder: sortOrder,
searchIndex: searchIndex,
},
];
fetchData(fetchParams);
@ -539,12 +558,22 @@ const Explore: React.FC<ExploreProps> = ({
searchResult.resAggDatabase.data.aggregations,
'database'
);
const aggDatabaseSchema = getAggregationList(
searchResult.resAggDatabaseSchema.data.aggregations,
'databaseschema'
);
const aggServiceName = getAggregationList(
searchResult.resAggServiceName.data.aggregations,
'servicename'
);
updateAggregationCount([
...aggServiceType,
...aggTier,
...aggTag,
...aggDatabase,
...aggDatabaseSchema,
...aggServiceName,
]);
}
setIsEntityLoading(false);
@ -573,7 +602,7 @@ const Explore: React.FC<ExploreProps> = ({
const fetchLeftPanel = () => {
return (
<>
<Fragment>
{!error && (
<FacetFilter
aggregations={getAggrWithDefaultValue(aggregations, visibleFilters)}
@ -583,7 +612,7 @@ const Explore: React.FC<ExploreProps> = ({
onSelectHandler={handleSelectedFilter}
/>
)}
</>
</Fragment>
);
};

View File

@ -80,6 +80,8 @@ const mockSearchResult = {
resAggTier: mockResponse as unknown as SearchResponse,
resAggTag: mockResponse as unknown as SearchResponse,
resAggDatabase: mockResponse as unknown as SearchResponse,
resAggDatabaseSchema: mockResponse as unknown as SearchResponse,
resAggServiceName: mockResponse as unknown as SearchResponse,
};
describe('Test Explore component', () => {

View File

@ -24,6 +24,8 @@ export type ExploreSearchData = {
resAggTier: SearchResponse;
resAggTag: SearchResponse;
resAggDatabase: SearchResponse;
resAggDatabaseSchema: SearchResponse;
resAggServiceName: SearchResponse;
};
export interface ExploreProps {

View File

@ -13,8 +13,9 @@
/* eslint-disable @typescript-eslint/camelcase */
import { getAllByTestId, render } from '@testing-library/react';
import { getAllByTestId, getByTestId, render } from '@testing-library/react';
import React from 'react';
import { facetFilterPlaceholder } from '../../../constants/constants';
import FacetFilter from './FacetFilter';
const onSelectHandler = jest.fn();
@ -22,27 +23,112 @@ const onClearFilter = jest.fn();
const onSelectAllFilter = jest.fn();
const aggregations = [
{
title: 'Filter 1',
buckets: [{ key: 'test', doc_count: 5 }],
title: 'Service',
buckets: [
{
key: 'BigQuery',
doc_count: 15,
},
{
key: 'Glue',
doc_count: 2,
},
],
},
{
title: 'Filter 2',
buckets: [{ key: 'test', doc_count: 5 }],
title: 'ServiceName',
buckets: [
{
key: 'bigquery_gcp',
doc_count: 15,
},
{
key: 'glue',
doc_count: 2,
},
],
},
{
title: 'Filter 3',
buckets: [{ key: 'test', doc_count: 5 }],
title: 'Tier',
buckets: [
{
key: 'Tier.Tier1',
doc_count: 0,
},
{
key: 'Tier.Tier2',
doc_count: 0,
},
{
key: 'Tier.Tier3',
doc_count: 0,
},
{
key: 'Tier.Tier4',
doc_count: 0,
},
{
key: 'Tier.Tier5',
doc_count: 0,
},
],
},
{
title: 'Tags',
buckets: [
{
key: 'PII.Sensitive',
doc_count: 1,
},
{
key: 'PersonalData.Personal',
doc_count: 1,
},
{
key: 'User.Address',
doc_count: 1,
},
],
},
{
title: 'Database',
buckets: [
{
key: 'ecommerce_db',
doc_count: 15,
},
{
key: 'default',
doc_count: 2,
},
],
},
{
title: 'DatabaseSchema',
buckets: [
{
key: 'shopify',
doc_count: 15,
},
{
key: 'information_schema',
doc_count: 2,
},
],
},
];
const filters = {
tags: ['test', 'test2'],
service: ['test', 'test2'],
'service type': ['test', 'test2'],
tier: ['test', 'test2'],
service: ['BigQuery', 'Glue'],
servicename: ['bigquery_gcp', 'glue'],
tier: ['Tier1', 'Tier2'],
tags: ['PII.Sensitive', 'User.Address'],
database: ['ecommerce_db', 'default'],
databaseschema: ['shopify', 'information_schema'],
};
describe('Test FacetFilter Component', () => {
it('Component should render', () => {
it('Should render all filters', () => {
const { container } = render(
<FacetFilter
aggregations={aggregations}
@ -54,6 +140,154 @@ describe('Test FacetFilter Component', () => {
);
const filterHeading = getAllByTestId(container, 'filter-heading');
expect(filterHeading.length).toBe(3);
expect(filterHeading.length).toBe(6);
});
it('Should render matching placeholder heading for filters', () => {
const { container } = render(
<FacetFilter
aggregations={aggregations}
filters={filters}
onClearFilter={onClearFilter}
onSelectAllFilter={onSelectAllFilter}
onSelectHandler={onSelectHandler}
/>
);
const filterHeading = getAllByTestId(container, 'filter-heading');
for (let index = 0; index < aggregations.length; index++) {
const element = aggregations[index];
const placeholder = facetFilterPlaceholder.find(
(p) => p.name === element.title
);
expect(filterHeading[index]).toHaveTextContent(placeholder?.value || '');
}
});
it('Should render all filters for service', () => {
const { container } = render(
<FacetFilter
aggregations={aggregations}
filters={filters}
onClearFilter={onClearFilter}
onSelectAllFilter={onSelectAllFilter}
onSelectHandler={onSelectHandler}
/>
);
const serviceAgg = getByTestId(container, aggregations[0].title);
for (let index = 0; index < aggregations[0].buckets.length; index++) {
const element = aggregations[0].buckets[index];
expect(
getByTestId(serviceAgg, `filter-container-${element.key}`)
).toBeInTheDocument();
}
});
it('Should render all filters for service name', () => {
const { container } = render(
<FacetFilter
aggregations={aggregations}
filters={filters}
onClearFilter={onClearFilter}
onSelectAllFilter={onSelectAllFilter}
onSelectHandler={onSelectHandler}
/>
);
const serviceNameAgg = getByTestId(container, aggregations[1].title);
for (let index = 0; index < aggregations[1].buckets.length; index++) {
const element = aggregations[1].buckets[index];
expect(
getByTestId(serviceNameAgg, `filter-container-${element.key}`)
).toBeInTheDocument();
}
});
it('Should render all filters for tier', () => {
const { container } = render(
<FacetFilter
aggregations={aggregations}
filters={filters}
onClearFilter={onClearFilter}
onSelectAllFilter={onSelectAllFilter}
onSelectHandler={onSelectHandler}
/>
);
const serviceNameAgg = getByTestId(container, aggregations[2].title);
for (let index = 0; index < aggregations[2].buckets.length; index++) {
const element = aggregations[2].buckets[index];
expect(
getByTestId(serviceNameAgg, `filter-container-${element.key}`)
).toBeInTheDocument();
}
});
it('Should render all filters for tags', () => {
const { container } = render(
<FacetFilter
aggregations={aggregations}
filters={filters}
onClearFilter={onClearFilter}
onSelectAllFilter={onSelectAllFilter}
onSelectHandler={onSelectHandler}
/>
);
const serviceNameAgg = getByTestId(container, aggregations[3].title);
for (let index = 0; index < aggregations[3].buckets.length; index++) {
const element = aggregations[3].buckets[index];
expect(
getByTestId(serviceNameAgg, `filter-container-${element.key}`)
).toBeInTheDocument();
}
});
it('Should render all filters for database', () => {
const { container } = render(
<FacetFilter
aggregations={aggregations}
filters={filters}
onClearFilter={onClearFilter}
onSelectAllFilter={onSelectAllFilter}
onSelectHandler={onSelectHandler}
/>
);
const serviceNameAgg = getByTestId(container, aggregations[4].title);
for (let index = 0; index < aggregations[4].buckets.length; index++) {
const element = aggregations[4].buckets[index];
expect(
getByTestId(serviceNameAgg, `filter-container-${element.key}`)
).toBeInTheDocument();
}
});
it('Should render all filters for database schema', () => {
const { container } = render(
<FacetFilter
aggregations={aggregations}
filters={filters}
onClearFilter={onClearFilter}
onSelectAllFilter={onSelectAllFilter}
onSelectHandler={onSelectHandler}
/>
);
const serviceNameAgg = getByTestId(container, aggregations[5].title);
for (let index = 0; index < aggregations[5].buckets.length; index++) {
const element = aggregations[5].buckets[index];
expect(
getByTestId(serviceNameAgg, `filter-container-${element.key}`)
).toBeInTheDocument();
}
});
});

View File

@ -12,7 +12,7 @@
*/
import classNames from 'classnames';
import { lowerCase } from 'lodash';
import { toLower } from 'lodash';
import { AggregationType, Bucket, FilterObject } from 'Models';
import PropTypes from 'prop-types';
import React, { Fragment, FunctionComponent, useState } from 'react';
@ -35,6 +35,10 @@ const FacetFilter: FunctionComponent<FacetProp> = ({
const [showAllServices, setShowAllServices] = useState<boolean>(false);
const [showAllTier, setShowAllTier] = useState<boolean>(false);
const [showAllDatabase, setShowAllDatabase] = useState<boolean>(false);
const [showAllDatabaseSchema, setShowAllDatabaseSchema] =
useState<boolean>(false);
const [showAllServiceName, setShowAllServiceName] = useState<boolean>(false);
const getLinkText = (
length: number,
state: boolean,
@ -87,6 +91,18 @@ const FacetFilter: FunctionComponent<FacetProp> = ({
return getLinkText(bucketLength, showAllTier, setShowAllTier);
case 'Database':
return getLinkText(bucketLength, showAllDatabase, setShowAllDatabase);
case 'DatabaseSchema':
return getLinkText(
bucketLength,
showAllDatabaseSchema,
setShowAllDatabaseSchema
);
case 'ServiceName':
return getLinkText(
bucketLength,
showAllServiceName,
setShowAllServiceName
);
default:
return null;
}
@ -103,6 +119,10 @@ const FacetFilter: FunctionComponent<FacetProp> = ({
return getBuckets(buckets, showAllTier);
case 'Database':
return getBuckets(buckets, showAllDatabase, true);
case 'DatabaseSchema':
return getBuckets(buckets, showAllDatabaseSchema, true);
case 'ServiceName':
return getBuckets(buckets, showAllServiceName, true);
default:
return [];
}
@ -115,11 +135,11 @@ const FacetFilter: FunctionComponent<FacetProp> = ({
<FilterContainer
count={bucket.doc_count}
isSelected={filters[
lowerCase(aggregation.title) as keyof FilterObject
toLower(aggregation.title) as keyof FilterObject
].includes(bucket.key)}
key={index}
name={bucket.key}
type={lowerCase(aggregation.title) as keyof FilterObject}
type={toLower(aggregation.title) as keyof FilterObject}
onSelect={onSelectHandler}
/>
)
@ -132,7 +152,7 @@ const FacetFilter: FunctionComponent<FacetProp> = ({
const isClearFilter = (aggregation: AggregationType) => {
const buckets = getBucketsByTitle(aggregation.title, aggregation.buckets);
const flag = buckets.some((bucket) =>
filters[lowerCase(aggregation.title) as keyof FilterObject].includes(
filters[toLower(aggregation.title) as keyof FilterObject].includes(
bucket.key
)
);
@ -143,7 +163,7 @@ const FacetFilter: FunctionComponent<FacetProp> = ({
const isSelectAllFilter = (aggregation: AggregationType) => {
const buckets = getBucketsByTitle(aggregation.title, aggregation.buckets);
const flag = buckets.every((bucket) =>
filters[lowerCase(aggregation.title) as keyof FilterObject].includes(
filters[toLower(aggregation.title) as keyof FilterObject].includes(
bucket.key
)
);
@ -182,7 +202,7 @@ const FacetFilter: FunctionComponent<FacetProp> = ({
return (
<Fragment key={index}>
{aggregation.buckets.length > 0 ? (
<>
<div data-testid={aggregation.title}>
<div className="tw-flex tw-justify-between tw-flex-col">
<h6
className="tw-heading tw-mb-0"
@ -200,9 +220,7 @@ const FacetFilter: FunctionComponent<FacetProp> = ({
onClick={() => {
if (isSelectAllFilter(aggregation)) {
onSelectAllFilter(
lowerCase(
aggregation.title
) as keyof FilterObject,
toLower(aggregation.title) as keyof FilterObject,
aggregation.buckets.map((b) => b.key)
);
}
@ -218,9 +236,7 @@ const FacetFilter: FunctionComponent<FacetProp> = ({
onClick={() => {
if (isClearFilter(aggregation)) {
onClearFilter(
lowerCase(
aggregation.title
) as keyof FilterObject
toLower(aggregation.title) as keyof FilterObject
);
}
}}>
@ -236,7 +252,7 @@ const FacetFilter: FunctionComponent<FacetProp> = ({
{getFilterItems(aggregation)}
</div>
{getSeparator(aggregations.length, index)}
</>
</div>
) : null}
</Fragment>
);

View File

@ -37,6 +37,7 @@ type Props = {
id?: string;
tier?: string | TagLabel;
usage?: number;
service?: string;
serviceType?: string;
fullyQualifiedName: string;
tags?: string[] | TagLabel[];
@ -46,6 +47,7 @@ type Props = {
value: number;
}[];
database?: string;
databaseSchema?: string;
deleted?: boolean;
};
@ -56,12 +58,14 @@ const TableDataCard: FunctionComponent<Props> = ({
id,
tier = '',
usage,
service,
serviceType,
fullyQualifiedName,
tags,
indexType,
matches,
database,
databaseSchema,
tableType,
deleted = false,
}: Props) => {
@ -95,7 +99,19 @@ const TableDataCard: FunctionComponent<Props> = ({
showLabel: true,
});
}
OtherDetails.push({ key: 'Service', value: serviceType, showLabel: true });
OtherDetails.push({
key: 'Service Type',
value: serviceType,
showLabel: true,
});
if (service) {
OtherDetails.push({
key: 'Service',
value: service,
showLabel: true,
});
}
if (database) {
OtherDetails.push({
key: 'Database',
@ -103,6 +119,13 @@ const TableDataCard: FunctionComponent<Props> = ({
showLabel: true,
});
}
if (databaseSchema) {
OtherDetails.push({
key: 'Schema',
value: databaseSchema,
showLabel: true,
});
}
const getAssetTags = () => {
const assetTags = [...(tags as Array<TagLabel>)];
if (tier && !isUndefined(tier)) {

View File

@ -25,6 +25,8 @@ describe('Test TableDataCardBody Component', () => {
{ key: 'Service', value: 'service' },
{ key: 'Usage', value: 'percentile' },
{ key: 'Tier', value: 'tier' },
{ key: 'Database', value: 'ecommerce_db' },
{ key: 'Schema', value: 'ecommerce_db' },
];
const tags = ['tag 1', 'tag 2', 'tag 3', 'tag 4'];
@ -67,4 +69,23 @@ describe('Test TableDataCardBody Component', () => {
expect(extraInfoTest).not.toBeInTheDocument();
});
it('Should render all the extra info', () => {
const { queryByText, getByTestId } = render(
<TableDataCardBody
description="test"
extraInfo={[...extraInfo, { key: 'undefined', value: undefined }]}
/>
);
const extraInformations = queryByText(/undefined/i);
expect(extraInformations).not.toBeInTheDocument();
for (let index = 0; index < extraInfo.length; index++) {
const element = extraInfo[index];
expect(getByTestId(`${element.key}`)).toBeInTheDocument();
}
});
});

View File

@ -49,10 +49,13 @@ const TableDataCardBody: FunctionComponent<Props> = ({
return (
<div data-testid="table-body">
<div className="tw-mb-4 tw-flex tw-items-center">
<div className="tw-mb-4 tw-flex tw-items-center tw-flex-wrap">
{extraInfo.map((info, i) =>
!isNil(info.value) ? (
<span className="tw-flex tw-items-center" key={i}>
<span
className="tw-flex tw-items-center"
data-testid={info.key}
key={i}>
{getInfoElements(info)}
{i !== extraInfo.length - 1 && (
<span className="tw-mx-1.5 tw-inline-block tw-text-gray-400">

View File

@ -118,6 +118,7 @@ const SearchedData: React.FC<SearchedDataProp> = ({
<div className="tw-mb-3" key={index}>
<TableDataCard
database={table.database}
databaseSchema={table.databaseSchema}
deleted={table.deleted}
description={tDesc}
fullyQualifiedName={table.fullyQualifiedName}
@ -126,6 +127,7 @@ const SearchedData: React.FC<SearchedDataProp> = ({
matches={matches}
name={name}
owner={getOwnerFromId(table.owner)?.name}
service={table.service}
serviceType={table.serviceType || '--'}
tableType={table.tableType as TableType}
tags={table.tags}

View File

@ -78,7 +78,14 @@ export const versionTypes = [
export const DESCRIPTIONLENGTH = 100;
export const visibleFilters = ['service', 'tier', 'tags', 'database'];
export const visibleFilters = [
'service',
'tier',
'tags',
'database',
'databaseschema',
'servicename',
];
export const tableSortingFields = [
{
@ -119,6 +126,14 @@ export const facetFilterPlaceholder = [
name: 'Database',
value: 'Database',
},
{
name: 'DatabaseSchema',
value: 'Schema',
},
{
name: 'ServiceName',
value: 'Service Name',
},
];
export const ROUTES = {

View File

@ -11,7 +11,7 @@
* limitations under the License.
*/
import { lowerCase } from 'lodash';
import { toLower } from 'lodash';
import { AggregationType, Bucket, FilterObject } from 'Models';
import { SearchIndex } from '../enums/search.enum';
import { getFilterKey } from '../utils/FilterUtils';
@ -26,6 +26,24 @@ export const emptyValue = '';
export const UPDATABLE_AGGREGATION = ['Service', 'Tier', 'Tags'];
export const FACET_FILTER_SORTING_ORDER = [
'Service',
'ServiceName',
'Tier',
'Tags',
'Database',
'DatabaseSchema',
];
export const INITIAL_FILTERS = {
tags: [],
service: [],
tier: [],
database: [],
databaseschema: [],
servicename: [],
};
export const getBucketList = (buckets: Array<Bucket>) => {
let bucketList: Array<Bucket> = [...tiers];
buckets.forEach((el) => {
@ -71,7 +89,7 @@ export const getAggrWithDefaultValue = (
(aggregation) => aggregation.title === 'Tier'
);
const allowedAgg = visibleAgg.map((item) => lowerCase(item));
const allowedAgg = visibleAgg.map((item) => toLower(item));
if (aggregation) {
const index = aggregations.indexOf(aggregation);
@ -80,17 +98,29 @@ export const getAggrWithDefaultValue = (
const visibleAggregations = !allowedAgg.length
? aggregations
: aggregations.filter((item) => allowedAgg.includes(lowerCase(item.title)));
: aggregations.filter((item) => allowedAgg.includes(toLower(item.title)));
return allowedAgg
const sortedAgg = allowedAgg
.map((agg) => {
const aggregation = visibleAggregations.find(
(a) => lowerCase(a.title) === agg
(a) => toLower(a.title) === agg
);
return aggregation;
})
.filter(Boolean) as Array<AggregationType>;
.filter(Boolean)
.sort((aggA, aggB) => {
if (
FACET_FILTER_SORTING_ORDER.indexOf(aggA?.title as string) >
FACET_FILTER_SORTING_ORDER.indexOf(aggB?.title as string)
) {
return 1;
} else {
return -1;
}
});
return sortedAgg as Array<AggregationType>;
};
export const getCurrentIndex = (tab: string) => {

View File

@ -205,6 +205,7 @@ declare module 'Models' {
};
index: string;
database?: string;
databaseSchema?: string;
deleted?: boolean;
entityType?: string;
};

View File

@ -186,6 +186,8 @@ const ExplorePage: FunctionComponent = () => {
resAggTier,
resAggTag,
resAggDatabase,
resAggDatabaseSchema,
resAggServiceName,
]: Array<SearchResponse>) => {
setError('');
setSearchResult({
@ -194,6 +196,8 @@ const ExplorePage: FunctionComponent = () => {
resAggTier,
resAggTag,
resAggDatabase,
resAggDatabaseSchema,
resAggServiceName,
});
setIsLoadingForData(false);
}

View File

@ -43,6 +43,7 @@ export const formatDataResponse = (hits) => {
newData.highlight = hit.highlight;
newData.database = hit._source.database;
newData.databaseSchema = hit._source.database_schema;
newData.entityType = hit._source.entity_type;
newData.changeDescriptions = hit._source.change_descriptions;

View File

@ -11,7 +11,7 @@
* limitations under the License.
*/
import { capitalize, lowerCase } from 'lodash';
import { capitalize, toLower } from 'lodash';
import { AggregationType, Sterm } from 'Models';
import { getQueryParam } from '../constants/explore.constants';
import { getFilterKey } from './FilterUtils';
@ -24,10 +24,7 @@ export const getAggregationList = (
const aggregationList: Array<AggregationType> = [];
aggrEntriesArr.forEach((aggr) => {
const aggrTitle = aggr[0].substring(aggr[0].indexOf('#') + 1);
if (
!aggregationType ||
lowerCase(aggrTitle) === lowerCase(aggregationType)
) {
if (!aggregationType || toLower(aggrTitle) === toLower(aggregationType)) {
aggregationList.push({
title: aggrTitle,
buckets: aggr[1].buckets,

View File

@ -11,6 +11,20 @@
* limitations under the License.
*/
const prepareModifiedKey = (key, restrictKeyModification = false) => {
if (!restrictKeyModification) {
if (key === 'service') {
return 'service type';
} else if (key === 'databaseschema') {
return 'database schema';
} else if (key === 'servicename') {
return 'service';
} else {
return key;
}
}
};
export const getFilterString = (
filters,
excludeFilters = [],
@ -24,8 +38,7 @@ export const getFilterString = (
const modifiedFilter = [];
const filter = filters[key];
filter.forEach((value) => {
const modifiedKey =
!restrictKeyModification && key === 'service' ? 'service type' : key;
const modifiedKey = prepareModifiedKey(key, restrictKeyModification);
const modifiedValue = key === 'tags' ? `"${value}"` : value;
modifiedFilter.push(
`${modifiedKey.split(' ').join('_')}:${modifiedValue}`