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}>
<ProfilerLatestValue {...profilerLatestValueProps} /> {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,77 +364,43 @@ 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}>
{overallSummary?.map((summary) => (
<Grid key={summary.title} size="grow">
<SummaryCardV1
extra={summary.extra}
icon={summary.icon}
isLoading={isLoading}
title={summary.title}
value={summary.value}
/>
</Grid>
))}
</Grid>
<Row gutter={[16, 16]}>
{!isUndefined(selectedColumn) && (
<Col span={10}>
<ColumnSummary column={selectedColumn} />
</Col>
)}
<Col span={selectedColumn ? 14 : 24}> <Grid container spacing={5}>
<Row {overallSummary?.map((summary) => (
wrap <Grid key={summary.title} size="grow">
className={classNames( <SummaryCardV1
activeColumnFqn ? 'justify-start' : 'justify-between' extra={summary.extra}
)} icon={summary.icon}
gutter={[16, 16]}> isLoading={isLoading}
{!isEmpty(activeColumnFqn) && title={summary.title}
map(selectedColumnTestsObj.statusDict, (data, key) => ( value={summary.value}
<Col key={key}> />
<SummaryCard </Grid>
showProgressBar ))}
isLoading={isTestCaseLoading} </Grid>
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} dataSource={data}
dataSource={data} expandable={getTableExpandableConfig<Column>()}
expandable={getTableExpandableConfig<Column>()} loading={isColumnsLoading || isLoading}
loading={isColumnsLoading || isLoading} locale={{
locale={{ emptyText: <FilterTablePlaceHolder />,
emptyText: <FilterTablePlaceHolder />, }}
}} pagination={false}
pagination={false} rowKey="name"
rowKey="name" scroll={{ x: true }}
scroll={{ x: true }} 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,115 +163,85 @@ 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}
name="count" name="count"
noDataPlaceholderText={noProfilerMessage} noDataPlaceholderText={noProfilerMessage}
title={t('label.data-count-plural')} title={t('label.data-count-plural')}
/> />
</Col> <ProfilerDetailsCard
<Col span={24}> chartCollection={columnMetric.proportionMetrics}
<ProfilerDetailsCard isLoading={isLoading}
chartCollection={columnMetric.proportionMetrics} name="proportion"
isLoading={isLoading} noDataPlaceholderText={noProfilerMessage}
name="proportion" tickFormatter="%"
noDataPlaceholderText={noProfilerMessage} title={t('label.data-proportion-plural')}
tickFormatter="%" />
title={t('label.data-proportion-plural')} <ProfilerDetailsCard
/> chartCollection={columnMetric.mathMetrics}
</Col> isLoading={isLoading}
<Col span={24}> name="math"
<ProfilerDetailsCard noDataPlaceholderText={noProfilerMessage}
chartCollection={columnMetric.mathMetrics} showYAxisCategory={isMinMaxStringData}
isLoading={isLoading} // only min/max category can be string
name="math" title={t('label.data-range')}
noDataPlaceholderText={noProfilerMessage} />
showYAxisCategory={isMinMaxStringData} <ProfilerDetailsCard
// only min/max category can be string chartCollection={columnMetric.sumMetrics}
title={t('label.data-range')} chartType="area"
/> isLoading={isLoading}
</Col> name="sum"
<Col span={24}> noDataPlaceholderText={noProfilerMessage}
<ProfilerDetailsCard title={t('label.data-aggregate')}
chartCollection={columnMetric.sumMetrics} />
isLoading={isLoading} <ProfilerDetailsCard
name="sum" chartCollection={columnMetric.quartileMetrics}
noDataPlaceholderText={noProfilerMessage} isLoading={isLoading}
title={t('label.data-aggregate')} name="quartile"
/> noDataPlaceholderText={noProfilerMessage}
</Col> title={t('label.data-quartile-plural')}
<Col span={24}> />
<ProfilerDetailsCard
chartCollection={columnMetric.quartileMetrics}
isLoading={isLoading}
name="quartile"
noDataPlaceholderText={noProfilerMessage}
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}> <DataDistributionHistogram
<Row gutter={[16, 16]}> data={{
<Col span={24}> firstDayData: firstDay,
<Typography.Title currentDayData: currentDay,
data-testid="data-distribution-title" }}
level={5}> noDataPlaceholderText={noProfilerMessage}
{t('label.data-distribution')} />
</Typography.Title> </ProfilerStateWrapper>
</Col>
<Col span={24}>
<DataDistributionHistogram
data={{ firstDayData: firstDay, currentDayData: currentDay }}
noDataPlaceholderText={noProfilerMessage}
/>
</Col>
</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}> <CardinalityDistributionChart
<Row gutter={[16, 16]}> data={{
<Col span={24}> firstDayData: firstDay,
<Typography.Title currentDayData: currentDay,
data-testid="cardinality-distribution-title" }}
level={5}> noDataPlaceholderText={noProfilerMessage}
{t('label.cardinality')} />
</Typography.Title> </ProfilerStateWrapper>
</Col>
<Col span={24}>
<CardinalityDistributionChart
data={{ firstDayData: firstDay, currentDayData: currentDay }}
noDataPlaceholderText={noProfilerMessage}
/>
</Col>
</Row>
</Card>
</Col>
) : null} ) : null}
<Col span={24}> <CustomMetricGraphs
<CustomMetricGraphs customMetrics={customMetrics}
customMetrics={customMetrics} customMetricsGraphData={columnCustomMetrics}
customMetricsGraphData={columnCustomMetrics} isLoading={isLoading || isProfilerDataLoading}
isLoading={isLoading || isProfilerDataLoading} />
/> </Stack>
</Col>
</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={{
<ErrorPlaceHolder placeholderText={noDataPlaceholderText} /> display: 'flex',
</Col> alignItems: 'center',
</Row> justifyContent: 'center',
height: '100%',
width: '100%',
}}>
<ErrorPlaceHolder placeholderText={noDataPlaceholderText} />
</Box>
); );
} }
@ -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,62 +169,96 @@ 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>
<ResponsiveContainer <Box sx={{ flex: 1, minHeight: 350 }}>
debounce={200} <ResponsiveContainer
id={`${key}-cardinality`} debounce={200}
minHeight={300}> id={`${key}-cardinality`}
<BarChart minHeight={300}>
className="w-full" <BarChart
data={graphData} className="w-full"
layout="vertical" data={graphData}
margin={{ left: 16 }}> layout="vertical"
<CartesianGrid stroke={GRAPH_BACKGROUND_COLOR} /> margin={{ left: 16 }}>
<XAxis <CartesianGrid
padding={{ left: 16, right: 16 }} stroke={GRAPH_BACKGROUND_COLOR}
tick={{ fontSize: 12 }} strokeDasharray="3 3"
tickFormatter={(props) => axisTickFormatter(props, '%')} vertical={false}
type="number" />
/> <XAxis
<YAxis axisLine={{
allowDataOverflow stroke: theme.palette.grey[200],
dataKey="name" }}
padding={{ top: 16, bottom: 16 }} padding={{ left: 16, right: 16 }}
tick={{ fontSize: 12 }} tick={{ fontSize: 12 }}
tickFormatter={(value: string) => tickFormatter={(props) => axisTickFormatter(props, '%')}
value?.length > 15 ? `${value.slice(0, 15)}...` : value tickLine={false}
} type="number"
type="category" />
width={120} <YAxis
/> allowDataOverflow
<Legend /> axisLine={false}
<Tooltip content={renderTooltip} /> dataKey="name"
<Bar dataKey="percentage" fill={CHART_BLUE_1} /> padding={{ top: 16, bottom: 16 }}
</BarChart> tick={{ fontSize: 12 }}
</ResponsiveContainer> tickFormatter={(value: string) =>
</Col> value?.length > 15 ? `${value.slice(0, 15)}...` : value
</Row> }
</Col> tickLine={false}
type="category"
width={120}
/>
<Legend />
<Tooltip
content={renderTooltip}
cursor={{
stroke: theme.palette.grey[200],
strokeDasharray: '3 3',
}}
/>
<Bar
dataKey="percentage"
fill={CHART_BLUE_1}
radius={[0, 8, 8, 0]}
/>
</BarChart>
</ResponsiveContainer>
</Box>
</Box>
); );
})} })}
</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={{
<ErrorPlaceHolder placeholderText={noDataPlaceholderText} /> display: 'flex',
</Col> alignItems: 'center',
</Row> justifyContent: 'center',
height: '100%',
width: '100%',
}}>
<ErrorPlaceHolder placeholderText={noDataPlaceholderText} />
</Box>
); );
} }
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>
<ResponsiveContainer <Box sx={{ flex: 1, minHeight: 350 }}>
debounce={200} <ResponsiveContainer
id={`${key}-histogram`} debounce={200}
minHeight={300}> height="100%"
<BarChart id={`${key}-histogram`}
className="w-full" width="100%">
data={graphData} <BarChart
margin={{ left: 16 }}> data={graphData}
<CartesianGrid stroke={GRAPH_BACKGROUND_COLOR} /> margin={{ top: 10, right: 10, bottom: 10, left: 10 }}>
<XAxis <CartesianGrid
dataKey="name" stroke={GRAPH_BACKGROUND_COLOR}
padding={{ left: 16, right: 16 }} strokeDasharray="3 3"
tick={{ fontSize: 12 }} vertical={false}
/> />
<YAxis <XAxis
allowDataOverflow axisLine={{
padding={{ top: 16, bottom: 16 }} stroke: theme.palette.grey[200],
tick={{ fontSize: 12 }} }}
tickFormatter={(props) => axisTickFormatter(props)} dataKey="name"
/> padding={{ left: 16, right: 16 }}
<Legend /> tick={{ fontSize: 12 }}
<Tooltip tickLine={false}
formatter={(value: number | string) => />
tooltipFormatter(value) <YAxis
} allowDataOverflow
/> axisLine={false}
<Bar dataKey="frequency" fill={CHART_BLUE_1} /> padding={{ top: 16, bottom: 16 }}
</BarChart> tick={{ fontSize: 12 }}
</ResponsiveContainer> tickFormatter={(props) => axisTickFormatter(props)}
</Col> tickLine={false}
</Row> />
</Col> <Legend />
<Tooltip
content={
<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]}
/>
</BarChart>
</ResponsiveContainer>
</Box>
</Box>
); );
})} })}
</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 (