MINOR: Data insight feedbacks (#17337)

* MINOR: DI add no tier in system chart

* checkstyle

* minor ui fixes

* tier filter fix

* ignore tags and glossaries from tier table

* enhance advance search provider

* Ignore data products as well

* Empty Filter Condition & Lowecase Owner Displayname

* opensearch

* sort graph data to confirm order of graph

* address comments

* handle domain in advanced filter

---------

Co-authored-by: Chira Madlani <chirag@getcollate.io>
Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com>
This commit is contained in:
Mayur Singal 2024-08-10 14:20:14 +05:30 committed by GitHub
parent ea3e111fbd
commit 52a7f99145
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 359 additions and 61 deletions

View File

@ -12,6 +12,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.glassfish.jersey.internal.util.ExceptionUtils; import org.glassfish.jersey.internal.util.ExceptionUtils;
import org.openmetadata.common.utils.CommonUtil; import org.openmetadata.common.utils.CommonUtil;
@ -37,6 +38,7 @@ public class DataInsightsEntityEnricherProcessor
implements Processor<List<Map<String, Object>>, ResultList<? extends EntityInterface>> { implements Processor<List<Map<String, Object>>, ResultList<? extends EntityInterface>> {
private final StepStats stats = new StepStats(); private final StepStats stats = new StepStats();
private static final Set<String> NON_TIER_ENTITIES = Set.of("tag", "glossaryTerm", "dataProduct");
public DataInsightsEntityEnricherProcessor(int total) { public DataInsightsEntityEnricherProcessor(int total) {
this.stats.withTotalRecords(total).withSuccessRecords(0).withFailedRecords(0); this.stats.withTotalRecords(total).withSuccessRecords(0).withFailedRecords(0);
@ -172,7 +174,15 @@ public class DataInsightsEntityEnricherProcessor
if (oEntityTags.isPresent()) { if (oEntityTags.isPresent()) {
Optional<String> oEntityTier = Optional<String> oEntityTier =
getEntityTier(oEntityTags.get().stream().map(TagLabel::getTagFQN).toList()); getEntityTier(oEntityTags.get().stream().map(TagLabel::getTagFQN).toList());
oEntityTier.ifPresent(s -> entityMap.put("tier", s)); oEntityTier.ifPresentOrElse(
s -> entityMap.put("tier", s),
() -> {
if (!NON_TIER_ENTITIES.contains(entityType)) {
entityMap.put("tier", "NoTier");
}
});
} else if (!NON_TIER_ENTITIES.contains(entityType)) {
entityMap.put("tier", "NoTier");
} }
// Enrich with Description Stats // Enrich with Description Stats

View File

@ -287,7 +287,10 @@ public class MigrationUtil {
dataInsightSystemChartRepository = new DataInsightSystemChartRepository(); dataInsightSystemChartRepository = new DataInsightSystemChartRepository();
String exclude_tags_filter = String exclude_tags_filter =
"{\"query\":{\"bool\":{\"must\":[{\"bool\":{\"must_not\":{\"term\":{\"entityType.keyword\":\"tag\"}}}},{\"bool\":{\"must_not\":{\"term\":{\"entityType.keyword\":\"glossaryTerm\"}}}}]}}}"; "{\"query\":{\"bool\":{\"must\":[{\"bool\":{\"must_not\":{\"term\":{\"entityType.keyword\":\"tag\"}}}},{\"bool\":{\"must_not\":{\"term\":{\"entityType.keyword\":\"glossaryTerm\"}}}},{\"bool\":{\"must_not\":{\"term\":{\"entityType.keyword\":\"dataProduct\"}}}}]}}}";
String exclude_tier_filter =
"{\"query\":{\"bool\":{\"must\":[{\"bool\":{\"must_not\":{\"term\":{\"entityType.keyword\":\"tag\"}}}},{\"bool\":{\"must_not\":{\"term\":{\"entityType.keyword\":\"glossaryTerm\"}}}},{\"bool\":{\"must_not\":{\"term\":{\"entityType.keyword\":\"dataProduct\"}}}},{\"bool\":{\"must_not\":{\"term\":{\"tier.keyword\":\"NoTier\"}}}}]}}}";
// total data assets // total data assets
List<String> excludeList = List.of("tag", "glossaryTerm"); List<String> excludeList = List.of("tag", "glossaryTerm");
createChart( createChart(
@ -359,7 +362,7 @@ public class MigrationUtil {
"total_data_assets_with_tier_summary_card", "total_data_assets_with_tier_summary_card",
new SummaryCard() new SummaryCard()
.withFormula("(count(k='id.keyword',q='tier.keyword: *')/count(k='id.keyword'))*100") .withFormula("(count(k='id.keyword',q='tier.keyword: *')/count(k='id.keyword'))*100")
.withFilter(exclude_tags_filter)); .withFilter(exclude_tier_filter));
// percentage of Data Asset with Description KPI // percentage of Data Asset with Description KPI
createChart( createChart(

View File

@ -147,7 +147,7 @@ public interface ElasticSearchDynamicChartAggregatorInterface {
throws IOException { throws IOException {
if (formula != null) { if (formula != null) {
if (filter != null) { if (filter != null && !filter.equals("{}")) {
XContentParser filterParser = XContentParser filterParser =
XContentType.JSON XContentType.JSON
.xContent() .xContent()
@ -162,7 +162,7 @@ public interface ElasticSearchDynamicChartAggregatorInterface {
// process non formula date histogram // process non formula date histogram
ValuesSourceAggregationBuilder subAgg = getSubAggregationsByFunction(function, field, 0); ValuesSourceAggregationBuilder subAgg = getSubAggregationsByFunction(function, field, 0);
if (filter != null) { if (filter != null && !filter.equals("{}")) {
XContentParser filterParser = XContentParser filterParser =
XContentType.JSON XContentType.JSON
.xContent() .xContent()

View File

@ -71,7 +71,7 @@ public class ElasticSearchSummaryCardAggregator
searchResponse.getAggregations().asList(), summaryCard.getFormula(), null, formulas); searchResponse.getAggregations().asList(), summaryCard.getFormula(), null, formulas);
List<DataInsightCustomChartResult> finalResults = new ArrayList<>(); List<DataInsightCustomChartResult> finalResults = new ArrayList<>();
for (int i = results.size() - 1; i >= 0; i++) { for (int i = results.size() - 1; i >= 0; i--) {
if (results.get(i).getCount() != null) { if (results.get(i).getCount() != null) {
finalResults.add(results.get(i)); finalResults.add(results.get(i));
resultList.setResults(finalResults); resultList.setResults(finalResults);

View File

@ -146,7 +146,7 @@ public interface OpenSearchDynamicChartAggregatorInterface {
List<FormulaHolder> formulas) List<FormulaHolder> formulas)
throws IOException { throws IOException {
if (formula != null) { if (formula != null) {
if (filter != null) { if (filter != null && !filter.equals("{}")) {
XContentParser filterParser = XContentParser filterParser =
XContentType.JSON XContentType.JSON
.xContent() .xContent()
@ -161,7 +161,7 @@ public interface OpenSearchDynamicChartAggregatorInterface {
// process non formula date histogram // process non formula date histogram
ValuesSourceAggregationBuilder subAgg = getSubAggregationsByFunction(function, field, 0); ValuesSourceAggregationBuilder subAgg = getSubAggregationsByFunction(function, field, 0);
if (filter != null) { if (filter != null && !filter.equals("{}")) {
XContentParser filterParser = XContentParser filterParser =
XContentType.JSON XContentType.JSON
.xContent() .xContent()

View File

@ -69,7 +69,7 @@ public class OpenSearchSummaryCardAggregator implements OpenSearchDynamicChartAg
searchResponse.getAggregations().asList(), summaryCard.getFormula(), null, formulas); searchResponse.getAggregations().asList(), summaryCard.getFormula(), null, formulas);
List<DataInsightCustomChartResult> finalResults = new ArrayList<>(); List<DataInsightCustomChartResult> finalResults = new ArrayList<>();
for (int i = results.size() - 1; i >= 0; i++) { for (int i = results.size() - 1; i >= 0; i--) {
if (results.get(i).getCount() != null) { if (results.get(i).getCount() != null) {
finalResults.add(results.get(i)); finalResults.add(results.get(i));
resultList.setResults(finalResults); resultList.setResults(finalResults);

View File

@ -1,9 +1,140 @@
{ {
"template": { "template": {
"settings": {
"analysis": {
"normalizer": {
"lowercase_normalizer": {
"type": "custom",
"char_filter": [],
"filter": [
"lowercase"
]
}
},
"analyzer": {
"om_analyzer": {
"tokenizer": "letter",
"filter": [
"lowercase",
"om_stemmer"
]
},
"om_ngram": {
"tokenizer": "ngram",
"min_gram": 3,
"max_gram": 10,
"filter": [
"lowercase"
]
}
},
"filter": {
"om_stemmer": {
"type": "stemmer",
"name": "english"
}
}
}
},
"mappings": { "mappings": {
"properties": { "properties": {
"@timestamp": { "@timestamp": {
"type": "date" "type": "date"
},
"owners": {
"properties": {
"id": {
"type": "keyword",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 36
}
}
},
"type": {
"type": "keyword"
},
"name": {
"type": "keyword",
"normalizer": "lowercase_normalizer",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"displayName": {
"type": "keyword",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase_normalizer",
"ignore_above": 256
}
}
},
"fullyQualifiedName": {
"type": "text"
},
"description": {
"type": "text"
},
"deleted": {
"type": "text"
},
"href": {
"type": "text"
}
}
},
"domain": {
"properties": {
"id": {
"type": "keyword",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 36
}
}
},
"type": {
"type": "keyword"
},
"name": {
"type": "keyword",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"displayName": {
"type": "keyword",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase_normalizer",
"ignore_above": 256
}
}
},
"fullyQualifiedName": {
"type": "keyword"
},
"description": {
"type": "text"
},
"deleted": {
"type": "text"
},
"href": {
"type": "text"
}
}
} }
} }
} }

View File

@ -1,9 +1,140 @@
{ {
"template": { "template": {
"settings": {
"analysis": {
"normalizer": {
"lowercase_normalizer": {
"type": "custom",
"char_filter": [],
"filter": [
"lowercase"
]
}
},
"analyzer": {
"om_analyzer": {
"tokenizer": "letter",
"filter": [
"lowercase",
"om_stemmer"
]
},
"om_ngram": {
"tokenizer": "ngram",
"min_gram": 3,
"max_gram": 10,
"filter": [
"lowercase"
]
}
},
"filter": {
"om_stemmer": {
"type": "stemmer",
"name": "english"
}
}
}
},
"mappings": { "mappings": {
"properties": { "properties": {
"@timestamp": { "@timestamp": {
"type": "date" "type": "date"
},
"owners": {
"properties": {
"id": {
"type": "keyword",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 36
}
}
},
"type": {
"type": "keyword"
},
"name": {
"type": "keyword",
"normalizer": "lowercase_normalizer",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"displayName": {
"type": "keyword",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase_normalizer",
"ignore_above": 256
}
}
},
"fullyQualifiedName": {
"type": "text"
},
"description": {
"type": "text"
},
"deleted": {
"type": "text"
},
"href": {
"type": "text"
}
}
},
"domain": {
"properties": {
"id": {
"type": "keyword",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 36
}
}
},
"type": {
"type": "keyword"
},
"name": {
"type": "keyword",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"displayName": {
"type": "keyword",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase_normalizer",
"ignore_above": 256
}
}
},
"fullyQualifiedName": {
"type": "keyword"
},
"description": {
"type": "text"
},
"deleted": {
"type": "text"
},
"href": {
"type": "text"
}
}
} }
} }
} }

View File

@ -18,7 +18,8 @@ import {
groupBy, groupBy,
includes, includes,
last, last,
map, omit,
reduce,
round, round,
sortBy, sortBy,
startCase, startCase,
@ -100,39 +101,50 @@ export const DataInsightChartCard = ({
const { rightSideEntityList, latestData, graphData, changeInValue } = const { rightSideEntityList, latestData, graphData, changeInValue } =
useMemo(() => { useMemo(() => {
const results = chartData.results ?? []; const results = chartData.results ?? [];
const timeStampResults = groupBy(results, 'day');
const groupedResults = groupBy(results, 'group'); const graphResults = Object.entries(timeStampResults).map(
const latestData: Record<string, number> = {}; ([key, value]) => {
let total = 0; const keys = value.reduce((acc, curr) => {
return { ...acc, [curr.group ?? 'count']: curr.count };
}, {});
let firstRecordTotal = 0; return {
day: +key,
...keys,
};
}
);
Object.entries(groupedResults).forEach(([key, value]) => { const finalData = sortBy(graphResults, 'day');
const newValues = sortBy(value, 'day');
latestData[key] = last(newValues)?.count ?? 0; const latestData: Record<string, number> = omit(
last(finalData ?? {}),
'day'
);
total += latestData[key]; const total = reduce(latestData, (acc, value) => acc + value, 0);
firstRecordTotal += first(newValues)?.count ?? 0;
}); const firstRecordTotal = reduce(
omit(first(finalData) ?? {}, 'day'),
(acc, value) => acc + value,
0
);
const uniqueLabels = Object.entries(latestData)
.sort(([, valueA], [, valueB]) => valueB - valueA)
.map(([key]) => key);
const changeInValue = firstRecordTotal const changeInValue = firstRecordTotal
? (total - firstRecordTotal) / firstRecordTotal ? (total - firstRecordTotal) / firstRecordTotal
: 0; : 0;
const graphData = map(groupedResults, (value, key) => ({
name: key,
data: value,
}));
const labels = Object.keys(groupedResults);
return { return {
rightSideEntityList: labels.filter((entity) => rightSideEntityList: uniqueLabels.filter((entity) =>
includes(toLower(entity), toLower(searchEntityKeyWord)) includes(toLower(entity), toLower(searchEntityKeyWord))
), ),
latestData, latestData,
graphData, graphData: finalData,
changeInValue, changeInValue,
}; };
}, [chartData.results, searchEntityKeyWord]); }, [chartData.results, searchEntityKeyWord]);
@ -267,6 +279,7 @@ export const DataInsightChartCard = ({
id={`${type}-graph`}> id={`${type}-graph`}>
{renderDataInsightLineChart( {renderDataInsightLineChart(
graphData, graphData,
rightSideEntityList,
activeKeys, activeKeys,
activeMouseHoverKey, activeMouseHoverKey,
isPercentageGraph isPercentageGraph

View File

@ -63,7 +63,7 @@ const DataInsightProgressBar = ({
className="data-insight-progress-bar" className="data-insight-progress-bar"
format={() => ( format={() => (
<> <>
{target && ( {target ? (
<span <span
className="data-insight-kpi-target" className="data-insight-kpi-target"
style={{ width: `${target}%` }}> style={{ width: `${target}%` }}>
@ -72,7 +72,7 @@ const DataInsightProgressBar = ({
{suffix} {suffix}
</span> </span>
</span> </span>
)} ) : null}
</> </>
)} )}
percent={progress} percent={progress}

View File

@ -30,7 +30,8 @@ export const AdvancedSearchModal: FunctionComponent<Props> = ({
onCancel, onCancel,
}: Props) => { }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { config, treeInternal, onTreeUpdate, onReset } = useAdvanceSearch(); const { config, treeInternal, onTreeUpdate, onReset, modalProps } =
useAdvanceSearch();
return ( return (
<Modal <Modal
@ -61,13 +62,16 @@ export const AdvancedSearchModal: FunctionComponent<Props> = ({
maskClosable={false} maskClosable={false}
okText={t('label.submit')} okText={t('label.submit')}
open={visible} open={visible}
title={t('label.advanced-entity', { title={
modalProps?.title ??
t('label.advanced-entity', {
entity: t('label.search'), entity: t('label.search'),
})} })
}
width={950} width={950}
onCancel={onCancel}> onCancel={onCancel}>
<Typography.Text data-testid="advanced-search-message"> <Typography.Text data-testid="advanced-search-message">
{t('message.advanced-search-message')} {modalProps?.subTitle ?? t('message.advanced-search-message')}
</Typography.Text> </Typography.Text>
<Query <Query
{...config} {...config}

View File

@ -29,7 +29,6 @@ import {
} from 'react-awesome-query-builder'; } from 'react-awesome-query-builder';
import { useHistory, useLocation, useParams } from 'react-router-dom'; import { useHistory, useLocation, useParams } from 'react-router-dom';
import { emptyJsonTree } from '../../../constants/AdvancedSearch.constants'; import { emptyJsonTree } from '../../../constants/AdvancedSearch.constants';
import { ROUTES } from '../../../constants/constants';
import { SearchIndex } from '../../../enums/search.enum'; import { SearchIndex } from '../../../enums/search.enum';
import { getTypeByFQN } from '../../../rest/metadataTypeAPI'; import { getTypeByFQN } from '../../../rest/metadataTypeAPI';
import advancedSearchClassBase from '../../../utils/AdvancedSearchClassBase'; import advancedSearchClassBase from '../../../utils/AdvancedSearchClassBase';
@ -52,6 +51,8 @@ const AdvancedSearchContext = React.createContext<AdvanceSearchContext>(
export const AdvanceSearchProvider = ({ export const AdvanceSearchProvider = ({
children, children,
isExplorePage = true,
modalProps,
}: AdvanceSearchProviderProps) => { }: AdvanceSearchProviderProps) => {
const tierOptions = useMemo(getTierOptions, []); const tierOptions = useMemo(getTierOptions, []);
@ -74,11 +75,6 @@ export const AdvanceSearchProvider = ({
return tabInfo[0] as SearchIndex; return tabInfo[0] as SearchIndex;
}, [tabsInfo, tab]); }, [tabsInfo, tab]);
const isExplorePage = useMemo(
() => location.pathname.startsWith(ROUTES.EXPLORE),
[location]
);
const [searchIndex, setSearchIndex] = useState< const [searchIndex, setSearchIndex] = useState<
SearchIndex | Array<SearchIndex> SearchIndex | Array<SearchIndex>
>(getSearchIndexFromTabInfo()); >(getSearchIndexFromTabInfo());
@ -331,6 +327,7 @@ export const AdvanceSearchProvider = ({
onUpdateConfig: handleConfigUpdate, onUpdateConfig: handleConfigUpdate,
onChangeSearchIndex: changeSearchIndex, onChangeSearchIndex: changeSearchIndex,
onSubmit: handleSubmit, onSubmit: handleSubmit,
modalProps,
}), }),
[ [
queryFilter, queryFilter,
@ -345,6 +342,7 @@ export const AdvanceSearchProvider = ({
handleConfigUpdate, handleConfigUpdate,
changeSearchIndex, changeSearchIndex,
handleSubmit, handleSubmit,
modalProps,
] ]
); );

View File

@ -16,6 +16,11 @@ import { SearchIndex } from '../../../enums/search.enum';
export interface AdvanceSearchProviderProps { export interface AdvanceSearchProviderProps {
children: ReactNode; children: ReactNode;
isExplorePage?: boolean;
modalProps?: {
title?: string;
subTitle?: string;
};
} }
export interface AdvanceSearchContext { export interface AdvanceSearchContext {
@ -31,6 +36,10 @@ export interface AdvanceSearchContext {
onChangeSearchIndex: (index: SearchIndex | Array<SearchIndex>) => void; onChangeSearchIndex: (index: SearchIndex | Array<SearchIndex>) => void;
searchIndex: string | Array<string>; searchIndex: string | Array<string>;
onSubmit: () => void; onSubmit: () => void;
modalProps?: {
title?: string;
subTitle?: string;
};
} }
export type FilterObject = Record<string, string[]>; export type FilterObject = Record<string, string[]>;

View File

@ -601,21 +601,18 @@ export const isPercentageSystemGraph = (graph: SystemChartType) => {
SystemChartType.PercentageOfDataAssetWithOwner, SystemChartType.PercentageOfDataAssetWithOwner,
SystemChartType.PercentageOfServiceWithDescription, SystemChartType.PercentageOfServiceWithDescription,
SystemChartType.PercentageOfServiceWithOwner, SystemChartType.PercentageOfServiceWithOwner,
SystemChartType.TotalDataAssetsByTier,
].includes(graph); ].includes(graph);
}; };
export const renderDataInsightLineChart = ( export const renderDataInsightLineChart = (
graphData: { graphData: Array<Record<string, number>>,
name: string; labels: string[],
data: { day: number; count: number }[];
}[],
activeKeys: string[], activeKeys: string[],
activeMouseHoverKey: string, activeMouseHoverKey: string,
isPercentage = true isPercentage: boolean
) => { ) => {
return ( return (
<LineChart margin={BAR_CHART_MARGIN}> <LineChart data={graphData} margin={BAR_CHART_MARGIN}>
<CartesianGrid stroke={GRAPH_BACKGROUND_COLOR} vertical={false} /> <CartesianGrid stroke={GRAPH_BACKGROUND_COLOR} vertical={false} />
<Tooltip <Tooltip
content={ content={
@ -630,24 +627,26 @@ export const renderDataInsightLineChart = (
type="category" type="category"
/> />
<YAxis <YAxis
dataKey="count" tickFormatter={
tickFormatter={(value: number) => axisTickFormatter(value, '%')} isPercentage
? (value: number) => axisTickFormatter(value, '%')
: undefined
}
/> />
{graphData.map((s, i) => ( {labels.map((s, i) => (
<Line <Line
data={s.data} dataKey={s}
dataKey="count"
hide={ hide={
activeKeys.length && s.name !== activeMouseHoverKey activeKeys.length && s !== activeMouseHoverKey
? !activeKeys.includes(s.name) ? !activeKeys.includes(s)
: false : false
} }
key={s.name} key={s}
name={s.name} name={s}
stroke={TOTAL_ENTITY_CHART_COLOR[i] ?? getRandomHexColor()} stroke={TOTAL_ENTITY_CHART_COLOR[i] ?? getRandomHexColor()}
strokeOpacity={ strokeOpacity={
isEmpty(activeMouseHoverKey) || s.name === activeMouseHoverKey isEmpty(activeMouseHoverKey) || s === activeMouseHoverKey
? DEFAULT_CHART_OPACITY ? DEFAULT_CHART_OPACITY
: HOVER_CHART_OPACITY : HOVER_CHART_OPACITY
} }
@ -674,10 +673,10 @@ export const getQueryFilterForDataInsightChart = (
bool: { bool: {
must: [ must: [
...(tierFilter ...(tierFilter
? [{ term: { 'tier.tagFQN': tierFilter } }] ? [{ term: { 'tier.keyword': tierFilter } }]
: []), : []),
...(teamFilter ...(teamFilter
? [{ term: { 'owner.displayName.keyword': teamFilter } }] ? [{ term: { 'owners.displayName.keyword': teamFilter } }]
: []), : []),
], ],
}, },