fix: improve application robustness and UI consistency

- Set default value for plugins in AppRouter to prevent potential errors.
- Add conditional rendering for ProfilerLatestValue to avoid rendering issues when props are undefined.
- Make profilerLatestValueProps optional in ProfilerStateWrapper interface for better flexibility.
- Refactor ColumnProfileTable to remove unused imports and optimize rendering logic.
- Replace Button with Typography for better styling in ColumnProfileTable.
- Update SingleColumnProfile to use Stack for layout consistency and include ColumnSummary when available.
- Enhance CardinalityDistributionChart and DataDistributionHistogram with new styling and layout using MUI components.
- Introduce DataPill styled component for consistent data display.
- Update color constants for improved visual consistency across charts.
- Modify data insight tooltip to conditionally display date in header for better clarity.
This commit is contained in:
Shailesh Parmar 2025-09-25 21:29:29 +05:30
parent f062908e56
commit 7ef9508f0a
12 changed files with 473 additions and 397 deletions

View File

@ -43,7 +43,7 @@ const AppRouter = () => {
isApplicationLoading, isApplicationLoading,
isAuthenticating, isAuthenticating,
} = useApplicationStore(); } = useApplicationStore();
const { plugins } = useApplicationsProvider(); const { plugins = [] } = useApplicationsProvider();
useEffect(() => { useEffect(() => {
const { pathname } = location; const { pathname } = location;

View File

@ -57,7 +57,9 @@ const ProfilerStateWrapper = ({
boxShadow: 'none', boxShadow: 'none',
}}> }}>
<Stack spacing={4}> <Stack spacing={4}>
{profilerLatestValueProps && (
<ProfilerLatestValue {...profilerLatestValueProps} /> <ProfilerLatestValue {...profilerLatestValueProps} />
)}
<Box flexGrow={1}>{children}</Box> <Box flexGrow={1}>{children}</Box>
</Stack> </Stack>
</Card> </Card>

View File

@ -16,6 +16,6 @@ export interface ProfilerStateWrapperProps {
isLoading: boolean; isLoading: boolean;
children: React.ReactNode; children: React.ReactNode;
title: string; title: string;
profilerLatestValueProps: ProfilerLatestValueProps; profilerLatestValueProps?: ProfilerLatestValueProps;
dataTestId?: string; dataTestId?: string;
} }

View File

@ -12,19 +12,8 @@
*/ */
import { Grid, Stack, Typography, useTheme } from '@mui/material'; import { Grid, Stack, Typography, useTheme } from '@mui/material';
import { Button, Col, Row } from 'antd';
import { ColumnsType } from 'antd/lib/table'; import { ColumnsType } from 'antd/lib/table';
import classNames from 'classnames'; import { isEmpty, round } from 'lodash';
import {
filter,
find,
groupBy,
isEmpty,
isUndefined,
map,
round,
toLower,
} from 'lodash';
import Qs from 'qs'; import Qs from 'qs';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -38,10 +27,6 @@ import {
Table as TableType, Table as TableType,
} from '../../../../../generated/entity/data/table'; } from '../../../../../generated/entity/data/table';
import { Operation } from '../../../../../generated/entity/policies/policy'; import { Operation } from '../../../../../generated/entity/policies/policy';
import {
TestCase,
TestCaseStatus,
} from '../../../../../generated/tests/testCase';
import { usePaging } from '../../../../../hooks/paging/usePaging'; import { usePaging } from '../../../../../hooks/paging/usePaging';
import useCustomLocation from '../../../../../hooks/useCustomLocation/useCustomLocation'; import useCustomLocation from '../../../../../hooks/useCustomLocation/useCustomLocation';
import { useFqn } from '../../../../../hooks/useFqn'; import { useFqn } from '../../../../../hooks/useFqn';
@ -49,26 +34,21 @@ import {
getTableColumnsByFQN, getTableColumnsByFQN,
searchTableColumnsByFQN, searchTableColumnsByFQN,
} from '../../../../../rest/tableAPI'; } from '../../../../../rest/tableAPI';
import { getListTestCaseBySearch } from '../../../../../rest/testAPI';
import { import {
formatNumberWithComma, formatNumberWithComma,
getTableFQNFromColumnFQN, getTableFQNFromColumnFQN,
} from '../../../../../utils/CommonUtils'; } from '../../../../../utils/CommonUtils';
import { getEntityName } from '../../../../../utils/EntityUtils'; import { getEntityName } from '../../../../../utils/EntityUtils';
import { import {
generateEntityLink,
getTableExpandableConfig, getTableExpandableConfig,
pruneEmptyChildren, pruneEmptyChildren,
} from '../../../../../utils/TableUtils'; } from '../../../../../utils/TableUtils';
import ErrorPlaceHolder from '../../../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; import ErrorPlaceHolder from '../../../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
import FilterTablePlaceHolder from '../../../../common/ErrorWithPlaceholder/FilterTablePlaceHolder'; import FilterTablePlaceHolder from '../../../../common/ErrorWithPlaceholder/FilterTablePlaceHolder';
import { PagingHandlerParams } from '../../../../common/NextPrevious/NextPrevious.interface'; import { PagingHandlerParams } from '../../../../common/NextPrevious/NextPrevious.interface';
import { SummaryCard } from '../../../../common/SummaryCard/SummaryCard.component';
import { SummaryCardProps } from '../../../../common/SummaryCard/SummaryCard.interface';
import SummaryCardV1 from '../../../../common/SummaryCard/SummaryCardV1'; import SummaryCardV1 from '../../../../common/SummaryCard/SummaryCardV1';
import Table from '../../../../common/Table/Table'; import Table from '../../../../common/Table/Table';
import { TableProfilerTab } from '../../ProfilerDashboard/profilerDashboard.interface'; import { TableProfilerTab } from '../../ProfilerDashboard/profilerDashboard.interface';
import ColumnSummary from '../ColumnSummary';
import NoProfilerBanner from '../NoProfilerBanner/NoProfilerBanner.component'; import NoProfilerBanner from '../NoProfilerBanner/NoProfilerBanner.component';
import SingleColumnProfile from '../SingleColumnProfile'; import SingleColumnProfile from '../SingleColumnProfile';
import { ModifiedColumn } from '../TableProfiler.interface'; import { ModifiedColumn } from '../TableProfiler.interface';
@ -95,8 +75,6 @@ const ColumnProfileTable = () => {
const isLoading = isTestsLoading || isProfilerDataLoading; const isLoading = isTestsLoading || isProfilerDataLoading;
const [searchText, setSearchText] = useState<string>(''); const [searchText, setSearchText] = useState<string>('');
const [data, setData] = useState<ModifiedColumn[]>([]); const [data, setData] = useState<ModifiedColumn[]>([]);
const [isTestCaseLoading, setIsTestCaseLoading] = useState(false);
const [columnTestCases, setColumnTestCases] = useState<TestCase[]>([]);
const [isColumnsLoading, setIsColumnsLoading] = useState(false); const [isColumnsLoading, setIsColumnsLoading] = useState(false);
const { const {
currentPage, currentPage,
@ -138,14 +116,20 @@ const ColumnProfileTable = () => {
fixed: 'left', fixed: 'left',
render: (_, record) => { render: (_, record) => {
return ( return (
<Button <Typography
className="break-word p-0" className="break-word p-0"
type="link" sx={{
color: theme.palette.primary.main,
fontSize: theme.typography.pxToRem(14),
fontWeight: theme.typography.fontWeightMedium,
cursor: 'pointer',
'&:hover': { textDecoration: 'underline' },
}}
onClick={() => onClick={() =>
updateActiveColumnFqn(record.fullyQualifiedName || '') updateActiveColumnFqn(record.fullyQualifiedName || '')
}> }>
{getEntityName(record)} {getEntityName(record)}
</Button> </Typography>
); );
}, },
sorter: (col1, col2) => col1.name.localeCompare(col2.name), sorter: (col1, col2) => col1.name.localeCompare(col2.name),
@ -157,7 +141,11 @@ const ColumnProfileTable = () => {
width: 150, width: 150,
render: (dataTypeDisplay: string) => { render: (dataTypeDisplay: string) => {
return ( return (
<Typography className="break-word"> <Typography
className="break-word"
sx={{
fontSize: theme.typography.pxToRem(14),
}}>
{dataTypeDisplay || 'N/A'} {dataTypeDisplay || 'N/A'}
</Typography> </Typography>
); );
@ -286,51 +274,11 @@ const ColumnProfileTable = () => {
]; ];
}, [testCaseSummary]); }, [testCaseSummary]);
const selectedColumn = useMemo(() => {
return find(
data,
(column: Column) => column.fullyQualifiedName === activeColumnFqn
);
}, [data, activeColumnFqn]);
const selectedColumnTestsObj = useMemo(() => {
const temp = filter(
columnTestCases,
(test: TestCase) => !isUndefined(test.testCaseResult)
);
const statusDict = {
[TestCaseStatus.Success]: [],
[TestCaseStatus.Aborted]: [],
[TestCaseStatus.Failed]: [],
...groupBy(temp, 'testCaseResult.testCaseStatus'),
};
return { statusDict, totalTests: temp.length };
}, [columnTestCases]);
const handleSearchAction = (searchText: string) => { const handleSearchAction = (searchText: string) => {
setSearchText(searchText); setSearchText(searchText);
handlePageChange(1); handlePageChange(1);
}; };
const fetchColumnTestCase = async (activeColumnFqn: string) => {
setIsTestCaseLoading(true);
try {
const { data } = await getListTestCaseBySearch({
fields: TabSpecificField.TEST_CASE_RESULT,
entityLink: generateEntityLink(activeColumnFqn),
limit: PAGE_SIZE_LARGE,
});
setColumnTestCases(data);
} catch {
setColumnTestCases([]);
} finally {
setIsTestCaseLoading(false);
}
};
const fetchTableColumnWithProfiler = useCallback( const fetchTableColumnWithProfiler = useCallback(
async (page: number, searchText: string) => { async (page: number, searchText: string) => {
if (!tableFqn) { if (!tableFqn) {
@ -382,14 +330,6 @@ const ColumnProfileTable = () => {
} }
}, [tableFqn, currentPage, searchText, pageSize]); }, [tableFqn, currentPage, searchText, pageSize]);
useEffect(() => {
if (activeColumnFqn) {
fetchColumnTestCase(activeColumnFqn);
} else {
setColumnTestCases([]);
}
}, [activeColumnFqn]);
const pagingProps = useMemo(() => { const pagingProps = useMemo(() => {
return { return {
currentPage: currentPage, currentPage: currentPage,
@ -424,7 +364,7 @@ const ColumnProfileTable = () => {
return ( return (
<Stack data-testid="column-profile-table-container" spacing="30px"> <Stack data-testid="column-profile-table-container" spacing="30px">
{!isLoading && !isProfilingEnabled && <NoProfilerBanner />} {!isLoading && !isProfilingEnabled && <NoProfilerBanner />}
<Col span={24}>
<Grid container spacing={5}> <Grid container spacing={5}>
{overallSummary?.map((summary) => ( {overallSummary?.map((summary) => (
<Grid key={summary.title} size="grow"> <Grid key={summary.title} size="grow">
@ -438,39 +378,8 @@ const ColumnProfileTable = () => {
</Grid> </Grid>
))} ))}
</Grid> </Grid>
<Row gutter={[16, 16]}>
{!isUndefined(selectedColumn) && (
<Col span={10}>
<ColumnSummary column={selectedColumn} />
</Col>
)}
<Col span={selectedColumn ? 14 : 24}>
<Row
wrap
className={classNames(
activeColumnFqn ? 'justify-start' : 'justify-between'
)}
gutter={[16, 16]}>
{!isEmpty(activeColumnFqn) &&
map(selectedColumnTestsObj.statusDict, (data, key) => (
<Col key={key}>
<SummaryCard
showProgressBar
isLoading={isTestCaseLoading}
title={key}
total={selectedColumnTestsObj.totalTests}
type={toLower(key) as SummaryCardProps['type']}
value={data.length}
/>
</Col>
))}
</Row>
</Col>
</Row>
</Col>
{isEmpty(activeColumnFqn) ? ( {isEmpty(activeColumnFqn) ? (
<Col span={24}>
<Table <Table
columns={tableColumn} columns={tableColumn}
customPaginationProps={pagingProps} customPaginationProps={pagingProps}
@ -486,15 +395,12 @@ const ColumnProfileTable = () => {
searchProps={searchProps} searchProps={searchProps}
size="small" size="small"
/> />
</Col>
) : ( ) : (
<Col span={24}>
<SingleColumnProfile <SingleColumnProfile
activeColumnFqn={activeColumnFqn} activeColumnFqn={activeColumnFqn}
dateRangeObject={dateRangeObject} dateRangeObject={dateRangeObject}
tableDetails={tableDetailsWithColumns} tableDetails={tableDetailsWithColumns}
/> />
</Col>
)} )}
</Stack> </Stack>
); );

View File

@ -10,9 +10,9 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { Card, Col, Row, Typography } from 'antd'; import { Stack } from '@mui/material';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { first, isString, last, pick } from 'lodash'; import { find, first, isString, last, pick } from 'lodash';
import { DateRangeObject } from 'Models'; import { DateRangeObject } from 'Models';
import { FC, useEffect, useMemo, useState } from 'react'; import { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -20,7 +20,10 @@ import {
DEFAULT_RANGE_DATA, DEFAULT_RANGE_DATA,
INITIAL_COLUMN_METRICS_VALUE, INITIAL_COLUMN_METRICS_VALUE,
} from '../../../../constants/profiler.constant'; } from '../../../../constants/profiler.constant';
import { ColumnProfile } from '../../../../generated/entity/data/container'; import {
Column,
ColumnProfile,
} from '../../../../generated/entity/data/container';
import { Table } from '../../../../generated/entity/data/table'; import { Table } from '../../../../generated/entity/data/table';
import { getColumnProfilerList } from '../../../../rest/tableAPI'; import { getColumnProfilerList } from '../../../../rest/tableAPI';
import { Transi18next } from '../../../../utils/CommonUtils'; import { Transi18next } from '../../../../utils/CommonUtils';
@ -35,6 +38,8 @@ import { showErrorToast } from '../../../../utils/ToastUtils';
import CardinalityDistributionChart from '../../../Visualisations/Chart/CardinalityDistributionChart.component'; import CardinalityDistributionChart from '../../../Visualisations/Chart/CardinalityDistributionChart.component';
import DataDistributionHistogram from '../../../Visualisations/Chart/DataDistributionHistogram.component'; import DataDistributionHistogram from '../../../Visualisations/Chart/DataDistributionHistogram.component';
import ProfilerDetailsCard from '../ProfilerDetailsCard/ProfilerDetailsCard'; import ProfilerDetailsCard from '../ProfilerDetailsCard/ProfilerDetailsCard';
import ProfilerStateWrapper from '../ProfilerStateWrapper/ProfilerStateWrapper.component';
import ColumnSummary from './ColumnSummary';
import CustomMetricGraphs from './CustomMetricGraphs/CustomMetricGraphs.component'; import CustomMetricGraphs from './CustomMetricGraphs/CustomMetricGraphs.component';
import { useTableProfiler } from './TableProfilerProvider'; import { useTableProfiler } from './TableProfilerProvider';
@ -63,6 +68,13 @@ const SingleColumnProfile: FC<SingleColumnProfileProps> = ({
[] []
); );
const selectedColumn = useMemo(() => {
return find(
tableDetails?.columns ?? [],
(column: Column) => column.fullyQualifiedName === activeColumnFqn
);
}, [tableDetails, activeColumnFqn]);
const customMetrics = useMemo( const customMetrics = useMemo(
() => () =>
getColumnCustomMetric( getColumnCustomMetric(
@ -151,11 +163,11 @@ const SingleColumnProfile: FC<SingleColumnProfileProps> = ({
}, [activeColumnFqn, dateRangeObject]); }, [activeColumnFqn, dateRangeObject]);
return ( return (
<Row <Stack
className="m-b-lg" className="m-b-lg"
data-testid="profiler-tab-container" data-testid="profiler-tab-container"
gutter={[16, 16]}> spacing="30px">
<Col span={24}> {selectedColumn && <ColumnSummary column={selectedColumn} />}
<ProfilerDetailsCard <ProfilerDetailsCard
chartCollection={columnMetric.countMetrics} chartCollection={columnMetric.countMetrics}
isLoading={isLoading} isLoading={isLoading}
@ -163,8 +175,6 @@ const SingleColumnProfile: FC<SingleColumnProfileProps> = ({
noDataPlaceholderText={noProfilerMessage} noDataPlaceholderText={noProfilerMessage}
title={t('label.data-count-plural')} title={t('label.data-count-plural')}
/> />
</Col>
<Col span={24}>
<ProfilerDetailsCard <ProfilerDetailsCard
chartCollection={columnMetric.proportionMetrics} chartCollection={columnMetric.proportionMetrics}
isLoading={isLoading} isLoading={isLoading}
@ -173,8 +183,6 @@ const SingleColumnProfile: FC<SingleColumnProfileProps> = ({
tickFormatter="%" tickFormatter="%"
title={t('label.data-proportion-plural')} title={t('label.data-proportion-plural')}
/> />
</Col>
<Col span={24}>
<ProfilerDetailsCard <ProfilerDetailsCard
chartCollection={columnMetric.mathMetrics} chartCollection={columnMetric.mathMetrics}
isLoading={isLoading} isLoading={isLoading}
@ -184,17 +192,14 @@ const SingleColumnProfile: FC<SingleColumnProfileProps> = ({
// only min/max category can be string // only min/max category can be string
title={t('label.data-range')} title={t('label.data-range')}
/> />
</Col>
<Col span={24}>
<ProfilerDetailsCard <ProfilerDetailsCard
chartCollection={columnMetric.sumMetrics} chartCollection={columnMetric.sumMetrics}
chartType="area"
isLoading={isLoading} isLoading={isLoading}
name="sum" name="sum"
noDataPlaceholderText={noProfilerMessage} noDataPlaceholderText={noProfilerMessage}
title={t('label.data-aggregate')} title={t('label.data-aggregate')}
/> />
</Col>
<Col span={24}>
<ProfilerDetailsCard <ProfilerDetailsCard
chartCollection={columnMetric.quartileMetrics} chartCollection={columnMetric.quartileMetrics}
isLoading={isLoading} isLoading={isLoading}
@ -202,64 +207,41 @@ const SingleColumnProfile: FC<SingleColumnProfileProps> = ({
noDataPlaceholderText={noProfilerMessage} noDataPlaceholderText={noProfilerMessage}
title={t('label.data-quartile-plural')} title={t('label.data-quartile-plural')}
/> />
</Col>
{firstDay?.histogram || currentDay?.histogram ? ( {firstDay?.histogram || currentDay?.histogram ? (
<Col span={24}> <ProfilerStateWrapper
<Card dataTestId="histogram-metrics"
className="shadow-none global-border-radius" isLoading={isLoading}
data-testid="histogram-metrics" title={t('label.data-distribution')}>
loading={isLoading}>
<Row gutter={[16, 16]}>
<Col span={24}>
<Typography.Title
data-testid="data-distribution-title"
level={5}>
{t('label.data-distribution')}
</Typography.Title>
</Col>
<Col span={24}>
<DataDistributionHistogram <DataDistributionHistogram
data={{ firstDayData: firstDay, currentDayData: currentDay }} data={{
firstDayData: firstDay,
currentDayData: currentDay,
}}
noDataPlaceholderText={noProfilerMessage} noDataPlaceholderText={noProfilerMessage}
/> />
</Col> </ProfilerStateWrapper>
</Row>
</Card>
</Col>
) : null} ) : null}
{firstDay?.cardinalityDistribution || {firstDay?.cardinalityDistribution ||
currentDay?.cardinalityDistribution ? ( currentDay?.cardinalityDistribution ? (
<Col span={24}> <ProfilerStateWrapper
<Card dataTestId="cardinality-distribution-metrics"
className="shadow-none global-border-radius" isLoading={isLoading}
data-testid="cardinality-distribution-metrics" title={t('label.cardinality')}>
loading={isLoading}>
<Row gutter={[16, 16]}>
<Col span={24}>
<Typography.Title
data-testid="cardinality-distribution-title"
level={5}>
{t('label.cardinality')}
</Typography.Title>
</Col>
<Col span={24}>
<CardinalityDistributionChart <CardinalityDistributionChart
data={{ firstDayData: firstDay, currentDayData: currentDay }} data={{
firstDayData: firstDay,
currentDayData: currentDay,
}}
noDataPlaceholderText={noProfilerMessage} noDataPlaceholderText={noProfilerMessage}
/> />
</Col> </ProfilerStateWrapper>
</Row>
</Card>
</Col>
) : null} ) : null}
<Col span={24}>
<CustomMetricGraphs <CustomMetricGraphs
customMetrics={customMetrics} customMetrics={customMetrics}
customMetricsGraphData={columnCustomMetrics} customMetricsGraphData={columnCustomMetrics}
isLoading={isLoading || isProfilerDataLoading} isLoading={isLoading || isProfilerDataLoading}
/> />
</Col> </Stack>
</Row>
); );
}; };

View File

@ -11,8 +11,8 @@
* limitations under the License. * limitations under the License.
*/ */
import { Card, Col, Row, Tag } from 'antd'; import { Box, Card, Divider, Typography, useTheme } from '@mui/material';
import { isUndefined, map } from 'lodash'; import { isUndefined } from 'lodash';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
Bar, Bar,
@ -30,6 +30,7 @@ import { GRAPH_BACKGROUND_COLOR } from '../../../constants/constants';
import { ColumnProfile } from '../../../generated/entity/data/table'; import { ColumnProfile } from '../../../generated/entity/data/table';
import { axisTickFormatter, tooltipFormatter } from '../../../utils/ChartUtils'; import { axisTickFormatter, tooltipFormatter } from '../../../utils/ChartUtils';
import { customFormatDateTime } from '../../../utils/date-time/DateTimeUtils'; import { customFormatDateTime } from '../../../utils/date-time/DateTimeUtils';
import { DataPill } from '../../common/DataPill/DataPill.styled';
import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
export interface CardinalityDistributionChartProps { export interface CardinalityDistributionChartProps {
@ -44,6 +45,7 @@ const CardinalityDistributionChart = ({
data, data,
noDataPlaceholderText, noDataPlaceholderText,
}: CardinalityDistributionChartProps) => { }: CardinalityDistributionChartProps) => {
const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
const showSingleGraph = const showSingleGraph =
isUndefined(data.firstDayData?.cardinalityDistribution) || isUndefined(data.firstDayData?.cardinalityDistribution) ||
@ -54,11 +56,16 @@ const CardinalityDistributionChart = ({
isUndefined(data.currentDayData?.cardinalityDistribution) isUndefined(data.currentDayData?.cardinalityDistribution)
) { ) {
return ( return (
<Row align="middle" className="h-full w-full" justify="center"> <Box
<Col> sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
width: '100%',
}}>
<ErrorPlaceHolder placeholderText={noDataPlaceholderText} /> <ErrorPlaceHolder placeholderText={noDataPlaceholderText} />
</Col> </Box>
</Row>
); );
} }
@ -70,16 +77,60 @@ const CardinalityDistributionChart = ({
const data = payload[0].payload; const data = payload[0].payload;
return ( return (
<Card> <Card
<p className="font-semibold text-sm mb-1">{`${t('label.category')}: ${ sx={{
data.name p: '10px',
}`}</p> bgcolor: theme.palette.allShades.white,
<p className="text-sm mb-1">{`${t('label.count')}: ${tooltipFormatter( }}>
data.count <Typography
)}`}</p> sx={{
<p className="text-sm">{`${t('label.percentage')}: ${ color: theme.palette.allShades.gray[900],
data.percentage fontWeight: theme.typography.fontWeightMedium,
}%`}</p> fontSize: theme.typography.pxToRem(12),
}}>
{data.name}
</Typography>
<Divider
sx={{
my: 2,
borderStyle: 'dashed',
borderColor: theme.palette.allShades.gray[300],
}}
/>
<Box className="d-flex items-center justify-between gap-6 p-b-xss text-sm">
<Typography
sx={(theme) => ({
color: theme.palette.allShades.gray[700],
fontSize: theme.typography.pxToRem(11),
})}>
{t('label.count')}
</Typography>
<Typography
sx={(theme) => ({
color: theme.palette.allShades.gray[900],
fontWeight: theme.typography.fontWeightMedium,
fontSize: theme.typography.pxToRem(11),
})}>
{tooltipFormatter(data.count)}
</Typography>
</Box>
<Box className="d-flex items-center justify-between gap-6 p-b-xss text-sm">
<Typography
sx={(theme) => ({
color: theme.palette.allShades.gray[700],
fontSize: theme.typography.pxToRem(11),
})}>
{t('label.percentage')}
</Typography>
<Typography
sx={(theme) => ({
color: theme.palette.allShades.gray[900],
fontWeight: theme.typography.fontWeightMedium,
fontSize: theme.typography.pxToRem(11),
})}>
{`${data.percentage}%`}
</Typography>
</Box>
</Card> </Card>
); );
} }
@ -87,9 +138,19 @@ const CardinalityDistributionChart = ({
return null; return null;
}; };
const dataEntries = Object.entries(data).filter(
([, columnProfile]) => !isUndefined(columnProfile?.cardinalityDistribution)
);
return ( return (
<Row className="w-full" data-testid="chart-container"> <Box
{map(data, (columnProfile, key) => { data-testid="chart-container"
sx={{
display: 'flex',
width: '100%',
gap: 0,
}}>
{dataEntries.map(([key, columnProfile], index) => {
if ( if (
isUndefined(columnProfile) || isUndefined(columnProfile) ||
isUndefined(columnProfile?.cardinalityDistribution) isUndefined(columnProfile?.cardinalityDistribution)
@ -108,24 +169,39 @@ const CardinalityDistributionChart = ({
const graphDate = customFormatDateTime( const graphDate = customFormatDateTime(
columnProfile?.timestamp || 0, columnProfile?.timestamp || 0,
'MMM dd' 'MMM dd, yyyy'
); );
return ( return (
<Col key={key} span={showSingleGraph ? 24 : 12}> <Box
<Row gutter={[8, 8]}> key={key}
<Col sx={{
data-testid="date" flex: showSingleGraph ? '1 1 100%' : '1 1 50%',
offset={showSingleGraph ? 1 : 2} minWidth: 0,
span={24}> display: 'flex',
{graphDate} flexDirection: 'column',
</Col> px: showSingleGraph ? 4 : 6,
<Col offset={showSingleGraph ? 1 : 2} span={24}> py: 2,
<Tag data-testid="cardinality-tag">{`${t('label.total-entity', { borderRight:
!showSingleGraph && index === 0
? `1px solid ${theme.palette.grey[200]}`
: 'none',
}}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
mb: 5,
}}>
<DataPill data-testid="date">{graphDate}</DataPill>
<DataPill data-testid="cardinality-tag">
{`${t('label.total-entity', {
entity: t('label.category-plural'), entity: t('label.category-plural'),
})}: ${cardinalityData.categories?.length || 0}`}</Tag> })}: ${cardinalityData.categories?.length || 0}`}
</Col> </DataPill>
<Col span={24}> </Box>
<Box sx={{ flex: 1, minHeight: 350 }}>
<ResponsiveContainer <ResponsiveContainer
debounce={200} debounce={200}
id={`${key}-cardinality`} id={`${key}-cardinality`}
@ -135,35 +211,54 @@ const CardinalityDistributionChart = ({
data={graphData} data={graphData}
layout="vertical" layout="vertical"
margin={{ left: 16 }}> margin={{ left: 16 }}>
<CartesianGrid stroke={GRAPH_BACKGROUND_COLOR} /> <CartesianGrid
stroke={GRAPH_BACKGROUND_COLOR}
strokeDasharray="3 3"
vertical={false}
/>
<XAxis <XAxis
axisLine={{
stroke: theme.palette.grey[200],
}}
padding={{ left: 16, right: 16 }} padding={{ left: 16, right: 16 }}
tick={{ fontSize: 12 }} tick={{ fontSize: 12 }}
tickFormatter={(props) => axisTickFormatter(props, '%')} tickFormatter={(props) => axisTickFormatter(props, '%')}
tickLine={false}
type="number" type="number"
/> />
<YAxis <YAxis
allowDataOverflow allowDataOverflow
axisLine={false}
dataKey="name" dataKey="name"
padding={{ top: 16, bottom: 16 }} padding={{ top: 16, bottom: 16 }}
tick={{ fontSize: 12 }} tick={{ fontSize: 12 }}
tickFormatter={(value: string) => tickFormatter={(value: string) =>
value?.length > 15 ? `${value.slice(0, 15)}...` : value value?.length > 15 ? `${value.slice(0, 15)}...` : value
} }
tickLine={false}
type="category" type="category"
width={120} width={120}
/> />
<Legend /> <Legend />
<Tooltip content={renderTooltip} /> <Tooltip
<Bar dataKey="percentage" fill={CHART_BLUE_1} /> content={renderTooltip}
cursor={{
stroke: theme.palette.grey[200],
strokeDasharray: '3 3',
}}
/>
<Bar
dataKey="percentage"
fill={CHART_BLUE_1}
radius={[0, 8, 8, 0]}
/>
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</Col> </Box>
</Row> </Box>
</Col>
); );
})} })}
</Row> </Box>
); );
}; };

View File

@ -11,8 +11,8 @@
* limitations under the License. * limitations under the License.
*/ */
import { Col, Row, Tag } from 'antd'; import { Box, useTheme } from '@mui/material';
import { isUndefined, map } from 'lodash'; import { isUndefined } from 'lodash';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
Bar, Bar,
@ -29,7 +29,9 @@ import { GRAPH_BACKGROUND_COLOR } from '../../../constants/constants';
import { DEFAULT_HISTOGRAM_DATA } from '../../../constants/profiler.constant'; import { DEFAULT_HISTOGRAM_DATA } from '../../../constants/profiler.constant';
import { HistogramClass } from '../../../generated/entity/data/table'; import { HistogramClass } from '../../../generated/entity/data/table';
import { axisTickFormatter, tooltipFormatter } from '../../../utils/ChartUtils'; import { axisTickFormatter, tooltipFormatter } from '../../../utils/ChartUtils';
import { CustomDQTooltip } from '../../../utils/DataQuality/DataQualityUtils';
import { customFormatDateTime } from '../../../utils/date-time/DateTimeUtils'; import { customFormatDateTime } from '../../../utils/date-time/DateTimeUtils';
import { DataPill } from '../../common/DataPill/DataPill.styled';
import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
import { DataDistributionHistogramProps } from './Chart.interface'; import { DataDistributionHistogramProps } from './Chart.interface';
@ -37,7 +39,9 @@ const DataDistributionHistogram = ({
data, data,
noDataPlaceholderText, noDataPlaceholderText,
}: DataDistributionHistogramProps) => { }: DataDistributionHistogramProps) => {
const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
const showSingleGraph = const showSingleGraph =
isUndefined(data.firstDayData?.histogram) || isUndefined(data.firstDayData?.histogram) ||
isUndefined(data.currentDayData?.histogram); isUndefined(data.currentDayData?.histogram);
@ -47,21 +51,32 @@ const DataDistributionHistogram = ({
isUndefined(data.currentDayData?.histogram) isUndefined(data.currentDayData?.histogram)
) { ) {
return ( return (
<Row align="middle" className="h-full w-full" justify="center"> <Box
<Col> sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
width: '100%',
}}>
<ErrorPlaceHolder placeholderText={noDataPlaceholderText} /> <ErrorPlaceHolder placeholderText={noDataPlaceholderText} />
</Col> </Box>
</Row>
); );
} }
return ( const dataEntries = Object.entries(data).filter(
<Row className="w-full" data-testid="chart-container"> ([, columnProfile]) => !isUndefined(columnProfile?.histogram)
{map(data, (columnProfile, key) => { );
if (isUndefined(columnProfile?.histogram)) {
return;
}
return (
<Box
data-testid="chart-container"
sx={{
display: 'flex',
width: '100%',
gap: 0,
}}>
{dataEntries.map(([key, columnProfile], index) => {
const histogramData = const histogramData =
(columnProfile?.histogram as HistogramClass) || (columnProfile?.histogram as HistogramClass) ||
DEFAULT_HISTOGRAM_DATA; DEFAULT_HISTOGRAM_DATA;
@ -73,59 +88,105 @@ const DataDistributionHistogram = ({
const graphDate = customFormatDateTime( const graphDate = customFormatDateTime(
columnProfile?.timestamp || 0, columnProfile?.timestamp || 0,
'MMM dd' 'MMM dd, yyyy'
); );
const skewColorTheme = columnProfile?.nonParametricSkew
? columnProfile?.nonParametricSkew > 0
? theme.palette.allShades.success
: theme.palette.allShades.error
: theme.palette.allShades.info;
return ( return (
<Col key={key} span={showSingleGraph ? 24 : 12}> <Box
<Row gutter={[8, 8]}> key={key}
<Col sx={{
data-testid="date" flex: showSingleGraph ? '1 1 100%' : '1 1 50%',
offset={showSingleGraph ? 1 : 2} minWidth: 0,
span={24}> display: 'flex',
{graphDate} flexDirection: 'column',
</Col> px: showSingleGraph ? 4 : 3,
<Col offset={showSingleGraph ? 1 : 2} span={24}> py: 2,
<Tag data-testid="skew-tag">{`${t('label.skew')}: ${ borderRight:
!showSingleGraph && index === 0
? `1px solid ${theme.palette.grey[200]}`
: 'none',
}}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
mb: 5,
}}>
<DataPill>{graphDate}</DataPill>
<DataPill
sx={{
backgroundColor: skewColorTheme[100],
color: skewColorTheme[900],
}}>
{`${t('label.skew')}: ${
columnProfile?.nonParametricSkew || '--' columnProfile?.nonParametricSkew || '--'
}`}</Tag> }`}
</Col> </DataPill>
<Col span={24}> </Box>
<Box sx={{ flex: 1, minHeight: 350 }}>
<ResponsiveContainer <ResponsiveContainer
debounce={200} debounce={200}
height="100%"
id={`${key}-histogram`} id={`${key}-histogram`}
minHeight={300}> width="100%">
<BarChart <BarChart
className="w-full"
data={graphData} data={graphData}
margin={{ left: 16 }}> margin={{ top: 10, right: 10, bottom: 10, left: 10 }}>
<CartesianGrid stroke={GRAPH_BACKGROUND_COLOR} /> <CartesianGrid
stroke={GRAPH_BACKGROUND_COLOR}
strokeDasharray="3 3"
vertical={false}
/>
<XAxis <XAxis
axisLine={{
stroke: theme.palette.grey[200],
}}
dataKey="name" dataKey="name"
padding={{ left: 16, right: 16 }} padding={{ left: 16, right: 16 }}
tick={{ fontSize: 12 }} tick={{ fontSize: 12 }}
tickLine={false}
/> />
<YAxis <YAxis
allowDataOverflow allowDataOverflow
axisLine={false}
padding={{ top: 16, bottom: 16 }} padding={{ top: 16, bottom: 16 }}
tick={{ fontSize: 12 }} tick={{ fontSize: 12 }}
tickFormatter={(props) => axisTickFormatter(props)} tickFormatter={(props) => axisTickFormatter(props)}
tickLine={false}
/> />
<Legend /> <Legend />
<Tooltip <Tooltip
formatter={(value: number | string) => content={
tooltipFormatter(value) <CustomDQTooltip
} displayDateInHeader={false}
timeStampKey="name"
valueFormatter={(value) => tooltipFormatter(value)}
/>
}
cursor={{
stroke: theme.palette.grey[200],
strokeDasharray: '3 3',
}}
/>
<Bar
dataKey="frequency"
fill={CHART_BLUE_1}
radius={[8, 8, 0, 0]}
/> />
<Bar dataKey="frequency" fill={CHART_BLUE_1} />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</Col> </Box>
</Row> </Box>
</Col>
); );
})} })}
</Row> </Box>
); );
}; };

View File

@ -0,0 +1,24 @@
/*
* Copyright 2023 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 { Box, styled } from '@mui/material';
export const DataPill = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.grey[100],
color: theme.palette.grey[900],
borderRadius: '6px',
padding: '6px 12px',
fontSize: theme.typography.pxToRem(14),
fontWeight: theme.typography.fontWeightBold,
display: 'inline-block',
}));

View File

@ -34,7 +34,7 @@ export const BLUE_2 = '#3ca2f4';
export const BLUE_500 = '#2E90FA'; export const BLUE_500 = '#2E90FA';
export const BLUE_800 = '#1849A9'; export const BLUE_800 = '#1849A9';
export const BLUE_50 = '#EFF8FF'; export const BLUE_50 = '#EFF8FF';
export const CHART_BLUE_1 = '#1890FF'; export const CHART_BLUE_1 = '#4689FF';
export const RIPTIDE = '#76E9C6'; export const RIPTIDE = '#76E9C6';
export const MY_SIN = '#FEB019'; export const MY_SIN = '#FEB019';
export const SAN_MARINO = '#416BB3'; export const SAN_MARINO = '#416BB3';

View File

@ -123,28 +123,28 @@ export const INITIAL_COUNT_METRIC_VALUE = {
entity: t('label.distinct'), entity: t('label.distinct'),
}), }),
dataKey: 'distinctCount', dataKey: 'distinctCount',
color: '#1890FF', color: '#467DDC',
}, },
{ {
title: t('label.entity-count', { title: t('label.entity-count', {
entity: t('label.null'), entity: t('label.null'),
}), }),
dataKey: 'nullCount', dataKey: 'nullCount',
color: '#7147E8', color: '#3488B5',
}, },
{ {
title: t('label.entity-count', { title: t('label.entity-count', {
entity: t('label.unique'), entity: t('label.unique'),
}), }),
dataKey: 'uniqueCount', dataKey: 'uniqueCount',
color: '#008376', color: '#685997',
}, },
{ {
title: t('label.entity-count', { title: t('label.entity-count', {
entity: t('label.value-plural'), entity: t('label.value-plural'),
}), }),
dataKey: 'valuesCount', dataKey: 'valuesCount',
color: '#B02AAC', color: '#464A52',
}, },
], ],
data: [], data: [],
@ -157,21 +157,21 @@ export const INITIAL_PROPORTION_METRIC_VALUE = {
entity: t('label.distinct'), entity: t('label.distinct'),
}), }),
dataKey: 'distinctProportion', dataKey: 'distinctProportion',
color: '#1890FF', color: '#6B97E3',
}, },
{ {
title: t('label.entity-proportion', { title: t('label.entity-proportion', {
entity: t('label.null'), entity: t('label.null'),
}), }),
dataKey: 'nullProportion', dataKey: 'nullProportion',
color: '#7147E8', color: '#867AAC',
}, },
{ {
title: t('label.entity-proportion', { title: t('label.entity-proportion', {
entity: t('label.unique'), entity: t('label.unique'),
}), }),
dataKey: 'uniqueProportion', dataKey: 'uniqueProportion',
color: '#008376', color: '#6B6E75',
}, },
], ],
data: [], data: [],
@ -182,17 +182,17 @@ export const INITIAL_MATH_METRIC_VALUE = {
{ {
title: t('label.max'), title: t('label.max'),
dataKey: 'max', dataKey: 'max',
color: '#1890FF', color: '#6B97E3',
}, },
{ {
title: t('label.mean'), title: t('label.mean'),
dataKey: 'mean', dataKey: 'mean',
color: '#7147E8', color: '#6B6E75',
}, },
{ {
title: t('label.min'), title: t('label.min'),
dataKey: 'min', dataKey: 'min',
color: '#008376', color: '#867AAC',
}, },
], ],
data: [], data: [],
@ -203,7 +203,8 @@ export const INITIAL_SUM_METRIC_VALUE = {
{ {
title: t('label.sum'), title: t('label.sum'),
dataKey: 'sum', dataKey: 'sum',
color: '#1890FF', color: BLUE_500,
fill: BLUE_50,
}, },
], ],
data: [], data: [],
@ -213,22 +214,22 @@ export const INITIAL_QUARTILE_METRIC_VALUE = {
{ {
title: t('label.first-quartile'), title: t('label.first-quartile'),
dataKey: 'firstQuartile', dataKey: 'firstQuartile',
color: '#1890FF', color: '#467DDC',
}, },
{ {
title: t('label.median'), title: t('label.median'),
dataKey: 'median', dataKey: 'median',
color: '#7147E8', color: '#3488B5',
}, },
{ {
title: t('label.inter-quartile-range'), title: t('label.inter-quartile-range'),
dataKey: 'interQuartileRange', dataKey: 'interQuartileRange',
color: '#008376', color: '#685997',
}, },
{ {
title: t('label.third-quartile'), title: t('label.third-quartile'),
dataKey: 'thirdQuartile', dataKey: 'thirdQuartile',
color: '#B02AAC', color: '#464A52',
}, },
], ],
data: [], data: [],

View File

@ -41,6 +41,7 @@ export interface ChartFilter {
export interface DataInsightChartTooltipProps extends TooltipProps<any, any> { export interface DataInsightChartTooltipProps extends TooltipProps<any, any> {
cardStyles?: React.CSSProperties; cardStyles?: React.CSSProperties;
customValueKey?: string; customValueKey?: string;
displayDateInHeader?: boolean;
dateTimeFormatter?: (date?: number, format?: string) => string; dateTimeFormatter?: (date?: number, format?: string) => string;
isPercentage?: boolean; isPercentage?: boolean;
isTier?: boolean; isTier?: boolean;

View File

@ -369,11 +369,15 @@ export const CustomDQTooltip = (props: DataInsightChartTooltipProps) => {
timeStampKey = 'timestampValue', timeStampKey = 'timestampValue',
transformLabel = true, transformLabel = true,
valueFormatter, valueFormatter,
displayDateInHeader = true,
} = props; } = props;
if (active && payload && payload.length) { if (active && payload && payload.length) {
// we need to check if the xAxis is a date or not. // we need to check if the xAxis is a date or not.
const timestamp = dateTimeFormatter(payload[0].payload[timeStampKey] || 0); const timestamp = displayDateInHeader
? dateTimeFormatter(payload[0].payload[timeStampKey] || 0)
: payload[0].payload[timeStampKey];
const payloadValue = uniqBy(payload, 'dataKey'); const payloadValue = uniqBy(payload, 'dataKey');
return ( return (