mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-01 11:09:14 +00:00
Sort data in total data assets widget by count (#23490)
(cherry picked from commit d8f8d6beb4a39df5021410d4811102a721924ceb)
This commit is contained in:
parent
7e6291697f
commit
1cf3649bcc
@ -13,7 +13,15 @@
|
|||||||
import { Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { groupBy, isEmpty, omit, reduce, sortBy, startCase } from 'lodash';
|
import {
|
||||||
|
groupBy,
|
||||||
|
isEmpty,
|
||||||
|
omit,
|
||||||
|
orderBy,
|
||||||
|
reduce,
|
||||||
|
sortBy,
|
||||||
|
startCase,
|
||||||
|
} from 'lodash';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
@ -75,13 +83,11 @@ const TotalDataAssetsWidget = ({
|
|||||||
applicationConfig?.customTheme?.primaryColor ??
|
applicationConfig?.customTheme?.primaryColor ??
|
||||||
DEFAULT_THEME.primaryColor;
|
DEFAULT_THEME.primaryColor;
|
||||||
|
|
||||||
|
// Generate palette and arrange from dark to light for high-to-low data
|
||||||
const fullPalette = generatePalette(primaryColor);
|
const fullPalette = generatePalette(primaryColor);
|
||||||
const reversed = fullPalette.slice().reverse();
|
|
||||||
|
|
||||||
const firstTwo = reversed.slice(0, 2);
|
// Reverse the palette to ensure dark-to-light order for high-to-low data
|
||||||
const remaining = reversed.slice(2);
|
return fullPalette.slice().reverse();
|
||||||
|
|
||||||
return [...remaining, ...firstTwo];
|
|
||||||
}, [applicationConfig?.customTheme?.primaryColor]);
|
}, [applicationConfig?.customTheme?.primaryColor]);
|
||||||
|
|
||||||
const widgetData = useMemo(() => {
|
const widgetData = useMemo(() => {
|
||||||
@ -92,67 +98,74 @@ const TotalDataAssetsWidget = ({
|
|||||||
return currentLayout?.find((item) => item.i === widgetKey)?.w === 2;
|
return currentLayout?.find((item) => item.i === widgetKey)?.w === 2;
|
||||||
}, [currentLayout, widgetKey]);
|
}, [currentLayout, widgetKey]);
|
||||||
|
|
||||||
const { graphData, rightSideEntityList, dataByDate, availableDates } =
|
const { graphData, dataByDate, availableDates } = useMemo(() => {
|
||||||
useMemo(() => {
|
const results = chartData?.results ?? [];
|
||||||
const results = chartData?.results ?? [];
|
|
||||||
|
|
||||||
const groupedByDay = groupBy(results, 'day');
|
const groupedByDay = groupBy(results, 'day');
|
||||||
const labels: string[] = [];
|
const labels: string[] = [];
|
||||||
|
|
||||||
const graphData = Object.entries(groupedByDay).map(
|
const graphData = Object.entries(groupedByDay).map(([dayKey, entries]) => {
|
||||||
([dayKey, entries]) => {
|
const day = Number(dayKey);
|
||||||
const day = Number(dayKey);
|
const values = entries.reduce((acc, curr) => {
|
||||||
const values = entries.reduce((acc, curr) => {
|
if (curr.group) {
|
||||||
if (curr.group) {
|
labels.push(curr.group);
|
||||||
labels.push(curr.group);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...acc,
|
|
||||||
[curr.group ?? 'count']: curr.count,
|
|
||||||
};
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return {
|
|
||||||
day,
|
|
||||||
dayString: customFormatDateTime(day, 'dd MMM'),
|
|
||||||
...values,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
const sortedData = sortBy(graphData, 'day');
|
return {
|
||||||
const uniqueLabels = Array.from(new Set(labels));
|
...acc,
|
||||||
|
[curr.group ?? 'count']: curr.count,
|
||||||
const dataByDate: Record<number, Record<string, number>> = {};
|
};
|
||||||
sortedData.forEach((item) => {
|
}, {});
|
||||||
dataByDate[item.day] = omit(item, ['day', 'dayString']);
|
|
||||||
});
|
|
||||||
|
|
||||||
const availableDates = sortedData.map(({ day, dayString }) => ({
|
|
||||||
day,
|
|
||||||
dayString,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
graphData: sortedData,
|
day,
|
||||||
rightSideEntityList: uniqueLabels,
|
dayString: customFormatDateTime(day, 'dd MMM'),
|
||||||
dataByDate,
|
...values,
|
||||||
availableDates,
|
|
||||||
};
|
};
|
||||||
}, [chartData?.results]);
|
});
|
||||||
|
|
||||||
const selectedDateData = useMemo(() => {
|
const sortedData = sortBy(graphData, 'day');
|
||||||
|
|
||||||
|
const dataByDate: Record<number, Record<string, number>> = {};
|
||||||
|
sortedData.forEach((item) => {
|
||||||
|
dataByDate[item.day] = omit(item, ['day', 'dayString']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const availableDates = sortedData.map(({ day, dayString }) => ({
|
||||||
|
day,
|
||||||
|
dayString,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
graphData: sortedData,
|
||||||
|
dataByDate,
|
||||||
|
availableDates,
|
||||||
|
};
|
||||||
|
}, [chartData?.results]);
|
||||||
|
|
||||||
|
const { selectedDateData, sortedEntityList, totalDatAssets } = useMemo(() => {
|
||||||
if (!selectedDate) {
|
if (!selectedDate) {
|
||||||
return {};
|
return { selectedDateData: {}, sortedEntityList: [], totalDatAssets: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
return dataByDate[selectedDate] ?? {};
|
const rawData = dataByDate[selectedDate] ?? {};
|
||||||
}, [selectedDate, dataByDate]);
|
|
||||||
|
|
||||||
const totalDatAssets = useMemo(() => {
|
// Sort data by count (high to low) and create sorted structures
|
||||||
return reduce(selectedDateData, (acc, value) => acc + value, 0);
|
const sortedEntries = orderBy(
|
||||||
}, [selectedDateData]);
|
Object.entries(rawData),
|
||||||
|
([, value]) => value,
|
||||||
|
'desc'
|
||||||
|
);
|
||||||
|
const sortedData = Object.fromEntries(sortedEntries);
|
||||||
|
const entityList = Object.keys(sortedData);
|
||||||
|
const total = reduce(sortedData, (acc, value) => acc + value, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedDateData: sortedData,
|
||||||
|
sortedEntityList: entityList,
|
||||||
|
totalDatAssets: total,
|
||||||
|
};
|
||||||
|
}, [selectedDate, dataByDate]);
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@ -222,7 +235,7 @@ const TotalDataAssetsWidget = ({
|
|||||||
nameKey="name"
|
nameKey="name"
|
||||||
outerRadius={117}
|
outerRadius={117}
|
||||||
paddingAngle={1}>
|
paddingAngle={1}>
|
||||||
{rightSideEntityList.map((label, index) => (
|
{sortedEntityList.map((label, index) => (
|
||||||
<Cell
|
<Cell
|
||||||
fill={pieChartColors[index % pieChartColors.length]}
|
fill={pieChartColors[index % pieChartColors.length]}
|
||||||
key={label}
|
key={label}
|
||||||
@ -252,11 +265,17 @@ const TotalDataAssetsWidget = ({
|
|||||||
|
|
||||||
{/* Right-side Legend */}
|
{/* Right-side Legend */}
|
||||||
{isFullSizeWidget && (
|
{isFullSizeWidget && (
|
||||||
<div className="flex-1 legend-list p-md">
|
<div
|
||||||
{rightSideEntityList.map((label, index) => (
|
className="flex-1 legend-list p-md"
|
||||||
<div className="d-flex items-center gap-3 text-sm" key={label}>
|
data-testid="assets-legend">
|
||||||
|
{sortedEntityList.map((label, index) => (
|
||||||
|
<div
|
||||||
|
className="d-flex items-center gap-3 text-sm"
|
||||||
|
data-testid={`legend-item-${label}`}
|
||||||
|
key={label}>
|
||||||
<span
|
<span
|
||||||
className="h-3 w-3"
|
className="h-3 w-3"
|
||||||
|
data-testid={`legend-color-${label}`}
|
||||||
style={{
|
style={{
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
@ -266,7 +285,9 @@ const TotalDataAssetsWidget = ({
|
|||||||
<Typography.Text ellipsis={{ tooltip: true }}>
|
<Typography.Text ellipsis={{ tooltip: true }}>
|
||||||
{startCase(label)}
|
{startCase(label)}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<span className="text-xs font-medium p-y-xss p-x-xs data-value">
|
<span
|
||||||
|
className="text-xs font-medium p-y-xss p-x-xs data-value"
|
||||||
|
data-testid={`legend-count-${label}`}>
|
||||||
{selectedDateData[label] ?? 0}
|
{selectedDateData[label] ?? 0}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -298,8 +319,9 @@ const TotalDataAssetsWidget = ({
|
|||||||
selectedDate,
|
selectedDate,
|
||||||
selectedDateData,
|
selectedDateData,
|
||||||
totalDatAssets,
|
totalDatAssets,
|
||||||
rightSideEntityList,
|
sortedEntityList,
|
||||||
isFullSizeWidget,
|
isFullSizeWidget,
|
||||||
|
pieChartColors,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -547,6 +547,79 @@ describe('TotalDataAssetsWidget', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Data Processing', () => {
|
describe('Data Processing', () => {
|
||||||
|
it('should sort data by count in descending order', async () => {
|
||||||
|
const sortedTestData: DataInsightCustomChartResult = {
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
count: 50,
|
||||||
|
day: 1640995200000,
|
||||||
|
group: 'dashboard',
|
||||||
|
term: 'dashboard',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: 200,
|
||||||
|
day: 1640995200000,
|
||||||
|
group: 'table',
|
||||||
|
term: 'table',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: 100,
|
||||||
|
day: 1640995200000,
|
||||||
|
group: 'topic',
|
||||||
|
term: 'topic',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
(getChartPreviewByName as jest.Mock).mockResolvedValueOnce(
|
||||||
|
sortedTestData
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
renderTotalDataAssetsWidget();
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('350')).toBeInTheDocument(); // Total count
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify legend items are sorted by count (high to low)
|
||||||
|
await waitFor(() => {
|
||||||
|
// Verify specific legend items exist with correct counts using data-testid
|
||||||
|
expect(screen.getByTestId('legend-count-table')).toHaveTextContent(
|
||||||
|
'200'
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('legend-count-topic')).toHaveTextContent(
|
||||||
|
'100'
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('legend-count-dashboard')).toHaveTextContent(
|
||||||
|
'50'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify legend items appear in sorted order (highest count first)
|
||||||
|
const legendContainer = screen.getByTestId('assets-legend');
|
||||||
|
const legendItems = legendContainer.querySelectorAll(
|
||||||
|
'[data-testid^="legend-item-"]'
|
||||||
|
);
|
||||||
|
|
||||||
|
// First item should be table (highest count)
|
||||||
|
expect(legendItems[0]).toHaveAttribute(
|
||||||
|
'data-testid',
|
||||||
|
'legend-item-table'
|
||||||
|
);
|
||||||
|
// Second item should be topic (medium count)
|
||||||
|
expect(legendItems[1]).toHaveAttribute(
|
||||||
|
'data-testid',
|
||||||
|
'legend-item-topic'
|
||||||
|
);
|
||||||
|
// Third item should be dashboard (lowest count)
|
||||||
|
expect(legendItems[2]).toHaveAttribute(
|
||||||
|
'data-testid',
|
||||||
|
'legend-item-dashboard'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should correctly group data by date', async () => {
|
it('should correctly group data by date', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
renderTotalDataAssetsWidget();
|
renderTotalDataAssetsWidget();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user