diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/ChangeInValueIndicator.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/ChangeInValueIndicator.tsx new file mode 100644 index 00000000000..5cb600c809a --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/ChangeInValueIndicator.tsx @@ -0,0 +1,47 @@ +/* + * Copyright 2022 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 { Typography } from 'antd'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +interface ChangeInValueIndicatorProps { + changeInValue: number; + suffix: string; + duration?: number; +} + +const ChangeInValueIndicator = ({ + changeInValue, + suffix, + duration, +}: ChangeInValueIndicatorProps) => { + const { t } = useTranslation(); + + return ( + + = 0 ? 'success' : 'danger'}> + {changeInValue >= 0 ? '+' : ''} + {changeInValue.toFixed(2)} + {suffix} + {' '} + {duration + ? t('label.days-change-lowercase', { + days: duration, + }) + : ''} + + ); +}; + +export default ChangeInValueIndicator; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/CustomStatistic.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/CustomStatistic.tsx new file mode 100644 index 00000000000..28e8676764d --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/CustomStatistic.tsx @@ -0,0 +1,56 @@ +/* + * Copyright 2021 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 { Space, Typography } from 'antd'; +import { isNil } from 'lodash'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import ChangeInValueIndicator from './ChangeInValueIndicator'; + +interface CustomStatisticProps { + value: number | string; + label?: string; + changeInValue?: number; + duration?: number; + suffix?: string; +} + +const CustomStatistic = ({ + value, + label, + changeInValue, + duration, + suffix = '%', +}: CustomStatisticProps) => { + const { t } = useTranslation(); + + return ( + + + {label ?? t('label.latest')} + + {value} + {changeInValue && !isNil(changeInValue) ? ( + + ) : ( + '' + )} + + ); +}; + +export default CustomStatistic; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/DailyActiveUsersChart.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/DailyActiveUsersChart.tsx index 221069a448d..eb40fa38486 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/DailyActiveUsersChart.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/DailyActiveUsersChart.tsx @@ -11,7 +11,7 @@ * limitations under the License. */ -import { Card, Typography } from 'antd'; +import { Card, Col, Row, Typography } from 'antd'; import { AxiosError } from 'axios'; import React, { FC, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -42,14 +42,16 @@ import { renderLegend, } from '../../utils/DataInsightUtils'; import { showErrorToast } from '../../utils/ToastUtils'; +import CustomStatistic from './CustomStatistic'; import './DataInsightDetail.less'; import { EmptyGraphPlaceholder } from './EmptyGraphPlaceholder'; interface Props { chartFilter: ChartFilter; + selectedDays: number; } -const DailyActiveUsersChart: FC = ({ chartFilter }) => { +const DailyActiveUsersChart: FC = ({ chartFilter, selectedDays }) => { const [dailyActiveUsers, setDailyActiveUsers] = useState( [] ); @@ -58,7 +60,7 @@ const DailyActiveUsersChart: FC = ({ chartFilter }) => { const { t } = useTranslation(); - const { data, total } = useMemo( + const { data, total, relativePercentage } = useMemo( () => getFormattedActiveUsersData(dailyActiveUsers), [dailyActiveUsers] ); @@ -102,28 +104,43 @@ const DailyActiveUsersChart: FC = ({ chartFilter }) => { }> {dailyActiveUsers.length ? ( - - - - - renderLegend({ payload: [] } as LegendProps, `${total}`) - } - layout="vertical" - verticalAlign="top" - wrapperStyle={{ left: '0px', top: '0px' }} + + + + + + + renderLegend({ payload: [] } as LegendProps, []) + } + layout="vertical" + verticalAlign="top" + wrapperStyle={{ left: '0px', top: '0px' }} + /> + + + } /> + + + + + + - - - } /> - - - + + ) : ( )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/DataInsightDetail.less b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/DataInsightDetail.less index c2c79984a53..174c1d13fe6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/DataInsightDetail.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/DataInsightDetail.less @@ -11,6 +11,8 @@ * limitations under the License. */ +@import url('../../styles/variables.less'); + @label-color: #6b7280; @summary-card-bg-hover: #f0f1f3; @@ -18,6 +20,14 @@ .ant-card-head { border-bottom: none; } + .custom-data-insight-tooltip { + .ant-card-head { + border-bottom: 1px solid @border-gray-color; + } + } + .ant-card-body { + padding-top: 8px; + } } .ant-typography.data-insight-label-text { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/DataInsightProgressBar.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/DataInsightProgressBar.tsx index 9b22ff984de..b1af2fd4444 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/DataInsightProgressBar.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/DataInsightProgressBar.tsx @@ -13,9 +13,11 @@ import { Progress, Typography } from 'antd'; import classNames from 'classnames'; +import { isNil } from 'lodash'; import React from 'react'; import { useTranslation } from 'react-i18next'; import SVGIcons, { Icons } from '../../utils/SvgUtils'; +import ChangeInValueIndicator from './ChangeInValueIndicator'; interface DataInsightProgressBarProps { width?: number; @@ -28,9 +30,13 @@ interface DataInsightProgressBarProps { successValue?: number | string; startValue?: number | string; suffix?: string; + changeInValue?: number; + duration?: number; + showEndValueAsLabel?: boolean; } const DataInsightProgressBar = ({ + showEndValueAsLabel = false, width, progress, className, @@ -41,17 +47,19 @@ const DataInsightProgressBar = ({ successValue = 100, showLabel = true, showSuccessInfo = false, + changeInValue, + duration, }: DataInsightProgressBarProps) => { const { t } = useTranslation(); return (
{showLabel && ( - + {label ?? t('label.latest')} - + )} -
+
( @@ -63,12 +71,16 @@ const DataInsightProgressBar = ({ {target && ( + style={{ width: `${target}%` }}> + + {target} + {suffix} + + )} {successValue} - {suffix} + {showEndValueAsLabel ? '' : suffix} )} @@ -79,6 +91,16 @@ const DataInsightProgressBar = ({ )}
+ + {changeInValue && !isNil(changeInValue) ? ( + + ) : ( + '' + )}
); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/DataInsightSummary.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/DataInsightSummary.tsx index 667d98e12a2..30f114799c3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/DataInsightSummary.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/DataInsightSummary.tsx @@ -179,6 +179,7 @@ const DataInsightSummary: FC = ({ chartFilter, onScrollToChart }) => { return ( = ({ chartFilter, onScrollToChart }) => { {summary.label} - + {summary.latest} {summary.id.startsWith('Percentage') || summary.id.includes(DataInsightChartType.TotalEntitiesByTier) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/DescriptionInsight.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/DescriptionInsight.test.tsx index 70f336307d0..78a3aae9aa1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/DescriptionInsight.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/DescriptionInsight.test.tsx @@ -49,6 +49,14 @@ jest.mock('../../utils/DataInsightUtils', () => ({ }, ], entities: ['Table', 'Topic', 'Database', 'Pipeline', 'Messaging'], + latestData: { + timestamp: '24/Oct', + Table: 0.3374, + Topic: 0.0353, + Database: 0.9774, + Pipeline: 0.4482, + Messaging: 0.3105, + }, })), })); @@ -65,6 +73,7 @@ describe('Test DescriptionInsight Component', () => { ); const card = screen.getByTestId('entity-description-percentage-card'); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/DescriptionInsight.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/DescriptionInsight.tsx index 9a40c65e339..d278aae2f07 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/DescriptionInsight.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/DescriptionInsight.tsx @@ -11,9 +11,9 @@ * limitations under the License. */ -import { Card, Typography } from 'antd'; +import { Card, Col, Row, Typography } from 'antd'; import { AxiosError } from 'axios'; -import { isEmpty } from 'lodash'; +import { isEmpty, uniqueId } from 'lodash'; import React, { FC, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -61,9 +61,10 @@ import { EmptyGraphPlaceholder } from './EmptyGraphPlaceholder'; interface Props { chartFilter: ChartFilter; kpi: Kpi | undefined; + selectedDays: number; } -const DescriptionInsight: FC = ({ chartFilter, kpi }) => { +const DescriptionInsight: FC = ({ chartFilter, kpi, selectedDays }) => { const [totalEntitiesDescriptionByType, setTotalEntitiesDescriptionByType] = useState(); @@ -71,7 +72,14 @@ const DescriptionInsight: FC = ({ chartFilter, kpi }) => { const [activeKeys, setActiveKeys] = useState([]); const [activeMouseHoverKey, setActiveMouseHoverKey] = useState(''); - const { data, entities, total } = useMemo(() => { + const { + data, + entities, + total, + relativePercentage, + latestData, + isPercentageGraph, + } = useMemo(() => { return getGraphDataByEntityType( totalEntitiesDescriptionByType?.data ?? [], DataInsightChartType.PercentageOfEntitiesWithDescriptionByType @@ -140,56 +148,97 @@ const DescriptionInsight: FC = ({ chartFilter, kpi }) => { }> - {data.length ? ( - - - - - axisTickFormatter(value, '%')} - /> - } /> - - renderLegend(props as LegendProps, total, activeKeys, false) - } - layout="vertical" - verticalAlign="top" - wrapperStyle={{ left: '0px', top: '0px' }} - onClick={handleLegendClick} - onMouseEnter={handleLegendMouseEnter} - onMouseLeave={handleLegendMouseLeave} - /> - {entities.map((entity) => ( - - ))} - - + + + + + + + + axisTickFormatter(value, '%') + } + /> + } /> + + renderLegend(props as LegendProps, activeKeys) + } + layout="horizontal" + verticalAlign="top" + wrapperStyle={{ left: '0px', top: '0px' }} + onClick={handleLegendClick} + onMouseEnter={handleLegendMouseEnter} + onMouseLeave={handleLegendMouseLeave} + /> + {entities.map((entity) => ( + + ))} + + + + + + + + {t('label.completed-description')} + {isPercentageGraph ? ' %' : ''} + + + + {entities.map((entity) => { + const progress = (latestData[entity] / Number(total)) * 100; + + return ( + + + + ); + })} + + + ) : ( )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/KPIChart.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/KPIChart.tsx index 5c717d5c39e..f8d2eee9660 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/KPIChart.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/KPIChart.tsx @@ -27,6 +27,8 @@ import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router-dom'; import { CartesianGrid, + Legend, + LegendProps, Line, LineChart, ResponsiveContainer, @@ -35,7 +37,12 @@ import { YAxis, } from 'recharts'; import { getLatestKpiResult, getListKpiResult } from '../../axiosAPIs/KpiAPI'; -import { GRAPH_BACKGROUND_COLOR, ROUTES } from '../../constants/constants'; +import { + DEFAULT_CHART_OPACITY, + GRAPH_BACKGROUND_COLOR, + HOVER_CHART_OPACITY, + ROUTES, +} from '../../constants/constants'; import { BAR_CHART_MARGIN, DATA_INSIGHT_GRAPH_COLORS, @@ -51,7 +58,12 @@ import { ChartFilter, UIKpiResult, } from '../../interface/data-insight.interface'; -import { CustomTooltip, getKpiGraphData } from '../../utils/DataInsightUtils'; +import { updateActiveChartFilter } from '../../utils/ChartUtils'; +import { + CustomTooltip, + getKpiGraphData, + renderLegend, +} from '../../utils/DataInsightUtils'; import { showErrorToast } from '../../utils/ToastUtils'; import './DataInsightDetail.less'; import { EmptyGraphPlaceholder } from './EmptyGraphPlaceholder'; @@ -71,6 +83,8 @@ const KPIChart: FC = ({ chartFilter, kpiList }) => { const [kpiLatestResults, setKpiLatestResults] = useState>(); const [isLoading, setIsLoading] = useState(false); + const [activeKeys, setActiveKeys] = useState([]); + const [activeMouseHoverKey, setActiveMouseHoverKey] = useState(''); const handleAddKpi = () => history.push(ROUTES.ADD_KPI); @@ -152,6 +166,18 @@ const KPIChart: FC = ({ chartFilter, kpiList }) => { return { ...getKpiGraphData(kpiResults, kpiList), kpiTooltipRecord }; }, [kpiResults, kpiList]); + const handleLegendClick: LegendProps['onClick'] = (event) => { + setActiveKeys((prevActiveKeys) => + updateActiveChartFilter(event.dataKey, prevActiveKeys) + ); + }; + const handleLegendMouseEnter: LegendProps['onMouseEnter'] = (event) => { + setActiveMouseHoverKey(event.dataKey); + }; + const handleLegendMouseLeave: LegendProps['onMouseLeave'] = () => { + setActiveMouseHoverKey(''); + }; + useEffect(() => { setKpiResults([]); setKpiLatestResults(undefined); @@ -183,16 +209,10 @@ const KPIChart: FC = ({ chartFilter, kpiList }) => { }> {kpiList.length ? ( - + {graphData.length ? ( <> - {!isUndefined(kpiLatestResults) && !isEmpty(kpiLatestResults) && ( - - - - )} - - + = ({ chartFilter, kpiList }) => { /> + + renderLegend(props as LegendProps, activeKeys) + } + layout="horizontal" + verticalAlign="top" + wrapperStyle={{ left: '0px', top: '0px' }} + onClick={handleLegendClick} + onMouseEnter={handleLegendMouseEnter} + onMouseLeave={handleLegendMouseLeave} + /> @@ -209,14 +241,30 @@ const KPIChart: FC = ({ chartFilter, kpiList }) => { {kpis.map((kpi, i) => ( ))} + {!isUndefined(kpiLatestResults) && !isEmpty(kpiLatestResults) && ( + + + + )} ) : ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/KPILatestResults.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/KPILatestResults.tsx index 1d3188b8d72..302eba04932 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/KPILatestResults.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/KPILatestResults.tsx @@ -46,25 +46,30 @@ const KPILatestResults: FC = ({ kpiLatestResultsRecord }) => { const isTargetMet = targetResult.targetMet; return ( - + {resultData.displayName ?? name} +
+ - - - - {getKpiResultFeedback(daysLeft, Boolean(isTargetMet))} - + + {getKpiResultFeedback(daysLeft, Boolean(isTargetMet))} + +
); })} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/OwnerInsight.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/OwnerInsight.tsx index 028c8185c15..e69a08d2306 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/OwnerInsight.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/OwnerInsight.tsx @@ -11,9 +11,9 @@ * limitations under the License. */ -import { Card, Typography } from 'antd'; +import { Card, Col, Row, Typography } from 'antd'; import { AxiosError } from 'axios'; -import { isEmpty } from 'lodash'; +import { isEmpty, uniqueId } from 'lodash'; import React, { FC, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -61,9 +61,10 @@ import { EmptyGraphPlaceholder } from './EmptyGraphPlaceholder'; interface Props { chartFilter: ChartFilter; kpi: Kpi | undefined; + selectedDays: number; } -const OwnerInsight: FC = ({ chartFilter, kpi }) => { +const OwnerInsight: FC = ({ chartFilter, kpi, selectedDays }) => { const [totalEntitiesOwnerByType, setTotalEntitiesOwnerByType] = useState(); @@ -71,7 +72,14 @@ const OwnerInsight: FC = ({ chartFilter, kpi }) => { const [activeKeys, setActiveKeys] = useState([]); const [activeMouseHoverKey, setActiveMouseHoverKey] = useState(''); - const { data, entities, total } = useMemo(() => { + const { + data, + entities, + total, + relativePercentage, + latestData, + isPercentageGraph, + } = useMemo(() => { return getGraphDataByEntityType( totalEntitiesOwnerByType?.data ?? [], DataInsightChartType.PercentageOfEntitiesWithOwnerByType @@ -139,53 +147,96 @@ const OwnerInsight: FC = ({ chartFilter, kpi }) => { }> - {data.length ? ( - - - - - axisTickFormatter(value, '%')} - /> - } /> - - renderLegend(props as LegendProps, total, activeKeys, true) - } - layout="vertical" - verticalAlign="top" - wrapperStyle={{ left: '0px', top: '0px' }} - onClick={handleLegendClick} - onMouseEnter={handleLegendMouseEnter} - onMouseLeave={handleLegendMouseLeave} - /> - {entities.map((entity) => ( - - ))} - - + + + + + + + + axisTickFormatter(value, '%') + } + /> + } /> + + renderLegend(props as LegendProps, activeKeys) + } + layout="horizontal" + verticalAlign="top" + wrapperStyle={{ left: '0px', top: '0px' }} + onClick={handleLegendClick} + onMouseEnter={handleLegendMouseEnter} + onMouseLeave={handleLegendMouseLeave} + /> + {entities.map((entity) => ( + + ))} + + + + + + + + {t('label.assigned-entity', { + entity: t('label.owner'), + })} + {isPercentageGraph ? ' %' : ''} + + + + {entities.map((entity) => { + const progress = (latestData[entity] / Number(total)) * 100; + + return ( + + + + ); + })} + + + ) : ( )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/PageViewsByEntitiesChart.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/PageViewsByEntitiesChart.tsx index 74d3b803b6f..375d6c90d7c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/PageViewsByEntitiesChart.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/PageViewsByEntitiesChart.tsx @@ -11,9 +11,9 @@ * limitations under the License. */ -import { Card, Typography } from 'antd'; +import { Card, Col, Row, Typography } from 'antd'; import { AxiosError } from 'axios'; -import { isEmpty } from 'lodash'; +import { isEmpty, uniqueId } from 'lodash'; import React, { FC, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -48,14 +48,17 @@ import { renderLegend, } from '../../utils/DataInsightUtils'; import { showErrorToast } from '../../utils/ToastUtils'; +import CustomStatistic from './CustomStatistic'; import './DataInsightDetail.less'; +import DataInsightProgressBar from './DataInsightProgressBar'; import { EmptyGraphPlaceholder } from './EmptyGraphPlaceholder'; interface Props { chartFilter: ChartFilter; + selectedDays: number; } -const PageViewsByEntitiesChart: FC = ({ chartFilter }) => { +const PageViewsByEntitiesChart: FC = ({ chartFilter, selectedDays }) => { const [pageViewsByEntities, setPageViewsByEntities] = useState(); @@ -63,12 +66,13 @@ const PageViewsByEntitiesChart: FC = ({ chartFilter }) => { const [activeKeys, setActiveKeys] = useState([]); const [activeMouseHoverKey, setActiveMouseHoverKey] = useState(''); - const { data, entities, total } = useMemo(() => { - return getGraphDataByEntityType( - pageViewsByEntities, - DataInsightChartType.PageViewsByEntities - ); - }, [pageViewsByEntities]); + const { data, entities, total, relativePercentage, latestData } = + useMemo(() => { + return getGraphDataByEntityType( + pageViewsByEntities, + DataInsightChartType.PageViewsByEntities + ); + }, [pageViewsByEntities]); const { t } = useTranslation(); @@ -124,44 +128,79 @@ const PageViewsByEntitiesChart: FC = ({ chartFilter }) => { }> {data.length ? ( - - - - - - } /> - - renderLegend(props as LegendProps, `${total}`, activeKeys) - } - layout="vertical" - verticalAlign="top" - wrapperStyle={{ left: '0px', top: '0px' }} - onClick={handleLegendClick} - onMouseEnter={handleLegendMouseEnter} - onMouseLeave={handleLegendMouseLeave} - /> - {entities.map((entity) => ( - - ))} - - + + + + + + + + } /> + + renderLegend(props as LegendProps, activeKeys) + } + layout="horizontal" + verticalAlign="top" + wrapperStyle={{ left: '0px', top: '0px' }} + onClick={handleLegendClick} + onMouseEnter={handleLegendMouseEnter} + onMouseLeave={handleLegendMouseLeave} + /> + {entities.map((entity) => ( + + ))} + + + + + + + + + {entities.map((entity) => { + const progress = (latestData[entity] / Number(total)) * 100; + + return ( + + + + ); + })} + + + ) : ( )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/TierInsight.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/TierInsight.tsx index 5313fd2c859..34b30d10a40 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/TierInsight.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/TierInsight.tsx @@ -11,9 +11,9 @@ * limitations under the License. */ -import { Card, Typography } from 'antd'; +import { Card, Col, Row, Typography } from 'antd'; import { AxiosError } from 'axios'; -import { isEmpty } from 'lodash'; +import { isEmpty, uniqueId } from 'lodash'; import React, { FC, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -56,9 +56,10 @@ import { EmptyGraphPlaceholder } from './EmptyGraphPlaceholder'; interface Props { chartFilter: ChartFilter; + selectedDays: number; } -const TierInsight: FC = ({ chartFilter }) => { +const TierInsight: FC = ({ chartFilter, selectedDays }) => { const [totalEntitiesByTier, setTotalEntitiesByTier] = useState(); @@ -66,7 +67,7 @@ const TierInsight: FC = ({ chartFilter }) => { const [activeKeys, setActiveKeys] = useState([]); const [activeMouseHoverKey, setActiveMouseHoverKey] = useState(''); - const { data, tiers, total } = useMemo(() => { + const { data, tiers, total, relativePercentage, latestData } = useMemo(() => { return getGraphDataByTierType(totalEntitiesByTier?.data ?? []); }, [totalEntitiesByTier]); @@ -122,50 +123,89 @@ const TierInsight: FC = ({ chartFilter }) => { }> - {data.length ? ( - - - - - - } /> - - renderLegend(props as LegendProps, total, activeKeys, false) - } - layout="vertical" - verticalAlign="top" - wrapperStyle={{ left: '0px', top: '0px' }} - onClick={handleLegendClick} - onMouseEnter={handleLegendMouseEnter} - onMouseLeave={handleLegendMouseLeave} - /> - {tiers.map((tier) => ( - - ))} - - + + + + + + + + } /> + + renderLegend(props as LegendProps, activeKeys) + } + layout="horizontal" + verticalAlign="top" + wrapperStyle={{ left: '0px', top: '0px' }} + onClick={handleLegendClick} + onMouseEnter={handleLegendMouseEnter} + onMouseLeave={handleLegendMouseLeave} + /> + {tiers.map((tier) => ( + + ))} + + + + + + + + {t('label.assigned-entity', { + entity: t('label.tier'), + })}{' '} + % + + + + {tiers.map((tiers) => { + const progress = (latestData[tiers] / Number(total)) * 100; + + return ( + + + + ); + })} + + + ) : ( )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/TotalEntityInsight.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/TotalEntityInsight.tsx index 7ed8ba53265..7ce052c880c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/TotalEntityInsight.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataInsightDetail/TotalEntityInsight.tsx @@ -11,9 +11,9 @@ * limitations under the License. */ -import { Card, Typography } from 'antd'; +import { Card, Col, Row, Typography } from 'antd'; import { AxiosError } from 'axios'; -import { isEmpty } from 'lodash'; +import { isEmpty, uniqueId } from 'lodash'; import React, { FC, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -50,14 +50,17 @@ import { renderLegend, } from '../../utils/DataInsightUtils'; import { showErrorToast } from '../../utils/ToastUtils'; +import CustomStatistic from './CustomStatistic'; import './DataInsightDetail.less'; +import DataInsightProgressBar from './DataInsightProgressBar'; import { EmptyGraphPlaceholder } from './EmptyGraphPlaceholder'; interface Props { chartFilter: ChartFilter; + selectedDays: number; } -const TotalEntityInsight: FC = ({ chartFilter }) => { +const TotalEntityInsight: FC = ({ chartFilter, selectedDays }) => { const [totalEntitiesByType, setTotalEntitiesByType] = useState(); @@ -65,12 +68,13 @@ const TotalEntityInsight: FC = ({ chartFilter }) => { const [activeKeys, setActiveKeys] = useState([]); const [activeMouseHoverKey, setActiveMouseHoverKey] = useState(''); - const { data, entities, total } = useMemo(() => { - return getGraphDataByEntityType( - totalEntitiesByType?.data ?? [], - DataInsightChartType.TotalEntitiesByType - ); - }, [totalEntitiesByType]); + const { data, entities, total, relativePercentage, latestData } = + useMemo(() => { + return getGraphDataByEntityType( + totalEntitiesByType?.data ?? [], + DataInsightChartType.TotalEntitiesByType + ); + }, [totalEntitiesByType]); const { t } = useTranslation(); @@ -125,44 +129,81 @@ const TotalEntityInsight: FC = ({ chartFilter }) => { }> {data.length ? ( - - - - - - } /> - - renderLegend(props as LegendProps, `${total}`, activeKeys) - } - layout="vertical" - verticalAlign="top" - wrapperStyle={{ left: '0px', top: '0px' }} - onClick={handleLegendClick} - onMouseEnter={handleLegendMouseEnter} - onMouseLeave={handleLegendMouseLeave} - /> - {entities.map((entity) => ( - - ))} - - + + + + + + + + } /> + + renderLegend(props as LegendProps, activeKeys) + } + layout="horizontal" + verticalAlign="top" + wrapperStyle={{ left: '0px', top: '0px' }} + onClick={handleLegendClick} + onMouseEnter={handleLegendMouseEnter} + onMouseLeave={handleLegendMouseLeave} + /> + {entities.map((entity) => ( + + ))} + + + + + + + + + {entities.map((entity) => { + const progress = (latestData[entity] / Number(total)) * 100; + + return ( + + + + ); + })} + + + ) : ( )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.less b/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.less index 23e706e54ac..4f768f3ea68 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.less @@ -54,6 +54,8 @@ } .quick-filter-dropdown-trigger-btn { + padding: 4px 8px; + .remove-field-icon { color: @remove-icon-color; padding-bottom: 2px; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/DataInsight.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/DataInsight.constants.ts index 467a82195f2..72d49d26fdb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/DataInsight.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/DataInsight.constants.ts @@ -23,8 +23,8 @@ import { export const BAR_CHART_MARGIN: Margin = { top: 20, - right: 30, - left: 20, + right: 20, + left: 10, bottom: 5, }; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts index a2159116d09..e98ee58c187 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts @@ -30,7 +30,7 @@ export const GRAPH_BACKGROUND_COLOR = '#f5f5f5'; export const GRAYED_OUT_COLOR = '#CCCCCC'; export const DEFAULT_CHART_OPACITY = 1; -export const HOVER_CHART_OPACITY = 0.5; +export const HOVER_CHART_OPACITY = 0.3; export const SUPPORTED_FIELD_TYPES = ['string', 'markdown', 'integer']; diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json index 82dce2bee13..2d26e646a0f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json @@ -577,7 +577,14 @@ "event-type": "Event Type", "connection-timeout-plural-optional": "Connection Timeout (s)", "all-data-assets": "All Data Assets", - "specific-data-assets": "Specific Data Assets" + "specific-data-assets": "Specific Data Assets", + "days-change-lowercase": "{{days}}-days change", + "total-entity": "Total {{entity}}", + "assets": "Assets", + "completed-description": "Completed Description", + "assigned-entity": "Assigned {{entity}}", + "total-assets-view": "Total Assets View", + "total-active-user": "Total Active User" }, "message": { "service-email-required": "Service account Email is required", diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/DataInsight.less b/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/DataInsight.less index 9657cb13826..568f963878a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/DataInsight.less +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/DataInsight.less @@ -17,6 +17,8 @@ @box-shadow-color: rgba(0, 0, 0, 0.06); @icon-color: #515151; @group-title-color: #76746f; +@summary-card-bg-hover: #f0f1f3; +@target-blue-color: #1890ff; .data-insight-select-dropdown { min-width: 136px; @@ -28,6 +30,7 @@ .data-insight-left-panel.ant-menu-root.ant-menu-inline { background: @white; border: 0px; + margin-top: 16px; .ant-menu-item::after { left: 0; @@ -87,6 +90,7 @@ // library has given height directly via style attribute , so to override need to provide !important height: 30px !important; } + .ant-progress-outer { margin-right: 0; padding-right: 0; @@ -109,12 +113,48 @@ left: 2px; padding: 0 16px 0 8px; font-size: 12px; + color: @text-color; } .data-insight-kpi-target { height: 100%; position: absolute; left: 0; - border-right: 2px solid #1890ff; - border-radius: 4px; + border-right: 2px solid @target-blue-color; + + .target-text { + position: relative; + right: -8px; + top: -14px; + display: flex; + justify-content: flex-end; + font-size: 10px; + } + + &::after { + content: '●'; + position: absolute; + top: -5px; + right: -6px; + color: @target-blue-color; + font-size: 10px; + } + } +} + +.custom-data-insight-legend { + display: flex; + gap: 4px; + + .custom-data-insight-legend-item { + display: flex; + align-items: center; + padding: 4px 8px; + cursor: pointer; + border-radius: 4px; + color: @text-grey-muted; + + &:hover { + background: @summary-card-bg-hover; + } } } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/DataInsightPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/DataInsightPage.component.tsx index 6655e77ed5b..364850bfa4c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/DataInsightPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/DataInsightPage.component.tsx @@ -94,6 +94,7 @@ const DataInsightPage = () => { const [chartFilter, setChartFilter] = useState(INITIAL_CHART_FILTER); const [kpiList, setKpiList] = useState>([]); + const [selectedDaysFilter, setSelectedDaysFilter] = useState(DEFAULT_DAYS); const [selectedChart, setSelectedChart] = useState(); @@ -127,6 +128,7 @@ const DataInsightPage = () => { }; const handleDaysChange = (days: number) => { + setSelectedDaysFilter(days); setChartFilter((previous) => ({ ...previous, startTs: getPastDaysDateTimeMillis(days), @@ -356,19 +358,30 @@ const DataInsightPage = () => { {activeTab === DataInsightTabs.DATA_ASSETS && ( <> - + - + - + )} @@ -378,10 +391,16 @@ const DataInsightPage = () => { - + - + diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DataInsightUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DataInsightUtils.tsx index 06beed559db..46ce21d2a94 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DataInsightUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DataInsightUtils.tsx @@ -15,6 +15,7 @@ import { Card, Typography } from 'antd'; import { RangePickerProps } from 'antd/lib/date-picker'; import { t } from 'i18next'; import { + first, groupBy, isEmpty, isInteger, @@ -52,7 +53,10 @@ import { KpiDates, } from '../interface/data-insight.interface'; import { pluralize } from './CommonUtils'; -import { getFormattedDateFromMilliSeconds } from './TimeUtils'; +import { + getDateByTimeStamp, + getFormattedDateFromMilliSeconds, +} from './TimeUtils'; const checkIsPercentageGraph = (dataInsightChartType: DataInsightChartType) => [ @@ -62,62 +66,46 @@ const checkIsPercentageGraph = (dataInsightChartType: DataInsightChartType) => export const renderLegend = ( legendData: LegendProps, - latest: string | number, - activeKeys = [] as string[], - showLatestValue = true + activeKeys = [] as string[] ) => { const { payload = [] } = legendData; return ( - <> - {showLatestValue && ( - <> - - Latest - - - {latest} - - - )} -
    - {payload.map((entry, index) => { - const isActive = - activeKeys.length === 0 || activeKeys.includes(entry.value); +
      + {payload.map((entry, index) => { + const isActive = + activeKeys.length === 0 || activeKeys.includes(entry.value); - return ( -
    • - legendData.onClick && legendData.onClick({ ...entry, ...e }) - } - onMouseEnter={(e) => - legendData.onMouseEnter && - legendData.onMouseEnter({ ...entry, ...e }) - } - onMouseLeave={(e) => - legendData.onMouseLeave && - legendData.onMouseLeave({ ...entry, ...e }) - }> - - - - - {entry.value} - -
    • - ); - })} -
    - + return ( +
  • + legendData.onClick && legendData.onClick({ ...entry, ...e }) + } + onMouseEnter={(e) => + legendData.onMouseEnter && + legendData.onMouseEnter({ ...entry, ...e }) + } + onMouseLeave={(e) => + legendData.onMouseLeave && + legendData.onMouseLeave({ ...entry, ...e }) + }> + + + + + {entry.value} + +
  • + ); + })} +
); }; @@ -157,20 +145,29 @@ const getEntryFormattedValue = ( }; export const CustomTooltip = (props: DataInsightChartTooltipProps) => { - const { active, payload = [], label, isPercentage, kpiTooltipRecord } = props; + const { active, payload = [], isPercentage, kpiTooltipRecord } = props; if (active && payload && payload.length) { + const timestamp = getDateByTimeStamp( + payload[0].payload.timestampValue || 0, + 'MMM dd, yyyy' + ); + return ( - - {/* this is a graph tooltip so using the explicit title here */} - {label} + {timestamp}}> {payload.map((entry, index) => ( -
  • - - - - - {entry.dataKey} -{' '} +
  • + + + + + {entry.dataKey} + + {getEntryFormattedValue( entry.value, entry.dataKey, @@ -226,7 +223,8 @@ const getLatestCount = (latestData = {}) => { const latestEntries = Object.entries(latestData ?? {}); for (const entry of latestEntries) { - if (entry[0] !== 'timestamp') { + // if key is 'timestamp' or 'timestampValue' skipping its count for total + if (!['timestamp', 'timestampValue'].includes(entry[0])) { total += toNumber(entry[1]); } } @@ -290,6 +288,62 @@ const getLatestPercentage = ( return 0; }; +/** + * + * @param rawData raw chart data + * @param dataInsightChartType chart type + * @returns old percentage for the chart + */ +const getOldestPercentage = ( + rawData: DataInsightChartResult['data'] = [], + dataInsightChartType: DataInsightChartType +) => { + let totalEntityCount = 0; + let totalEntityWithDescription = 0; + let totalEntityWithOwner = 0; + + const modifiedData = rawData + .map((raw) => { + const timestamp = raw.timestamp; + if (timestamp) { + return { + ...raw, + timestamp, + }; + } + + return; + }) + .filter(Boolean); + + const sortedData = sortBy(modifiedData, 'timestamp'); + const groupDataByTimeStamp = groupBy(sortedData, 'timestamp'); + const oldestData = first(sortedData); + if (oldestData) { + const oldestChartRecords = groupDataByTimeStamp[oldestData.timestamp]; + + oldestChartRecords.forEach((record) => { + totalEntityCount += record?.entityCount ?? 0; + totalEntityWithDescription += record?.completedDescription ?? 0; + totalEntityWithOwner += record?.hasOwner ?? 0; + }); + switch (dataInsightChartType) { + case DataInsightChartType.PercentageOfEntitiesWithDescriptionByType: + return ((totalEntityWithDescription / totalEntityCount) * 100).toFixed( + 2 + ); + + case DataInsightChartType.PercentageOfEntitiesWithOwnerByType: + return ((totalEntityWithOwner / totalEntityCount) * 100).toFixed(2); + + default: + return 0; + } + } + + return 0; +}; + /** * * @param rawData raw chart data @@ -341,6 +395,7 @@ const getGraphFilteredData = ( return { timestamp: timestamp, + timestampValue: data.timestamp, [data.entityType]: value, }; } @@ -370,7 +425,20 @@ export const getGraphDataByEntityType = ( ); const graphData = prepareGraphData(timestamps, filteredData); - const latestData = last(graphData); + const latestData = last(graphData) as Record; + const oldData = first(graphData); + const latestPercentage = toNumber( + isPercentageGraph + ? getLatestPercentage(rawData, dataInsightChartType) + : getLatestCount(latestData) + ); + const oldestPercentage = toNumber( + isPercentageGraph + ? getOldestPercentage(rawData, dataInsightChartType) + : getLatestCount(oldData) + ); + + const relativePercentage = latestPercentage - oldestPercentage; return { data: graphData, @@ -378,6 +446,11 @@ export const getGraphDataByEntityType = ( total: isPercentageGraph ? getLatestPercentage(rawData, dataInsightChartType) : getLatestCount(latestData), + relativePercentage: isPercentageGraph + ? relativePercentage + : (relativePercentage / oldestPercentage) * 100, + latestData, + isPercentageGraph, }; }; @@ -403,6 +476,7 @@ export const getGraphDataByTierType = (rawData: TotalEntitiesByTier[]) => { } return { + timestampValue: data.timestamp, timestamp: timestamp, [tiering]: ((data?.entityCountFraction || 0) * 100).toFixed(2), }; @@ -412,12 +486,16 @@ export const getGraphDataByTierType = (rawData: TotalEntitiesByTier[]) => { }); const graphData = prepareGraphData(timestamps, filteredData); - const latestData = last(graphData); + const latestData = getLatestCount(last(graphData)); + const oldestData = getLatestCount(first(graphData)); + const relativePercentage = latestData - oldestData; return { data: graphData, tiers, - total: getLatestCount(latestData), + total: latestData, + relativePercentage, + latestData: last(graphData) as Record, }; }; @@ -430,14 +508,21 @@ export const getFormattedActiveUsersData = ( ) => { const formattedData = activeUsers.map((user) => ({ ...user, + timestampValue: user.timestamp, timestamp: user.timestamp ? getFormattedDateFromMilliSeconds(user.timestamp) : '', })); + const latestCount = Number(last(formattedData)?.activeUsers); + const oldestCount = Number(first(formattedData)?.activeUsers); + + const relativePercentage = ((latestCount - oldestCount) / oldestCount) * 100; + return { data: formattedData, - total: last(formattedData)?.activeUsers, + total: latestCount, + relativePercentage, }; }; @@ -518,6 +603,7 @@ export const getKpiGraphData = (kpiResults: KpiResult[], kpiList: Kpi[]) => { } return { + timestampValue: kpiResult.timestamp, timestamp, [kpiFqn]: currentKpi?.metricType === KpiTargetType.Percentage