[UI] Data Insight Layout looks broken in case of large number of assets #13803 (#13921)

* [UI] Data Insight Layout looks broken in case of large number of assets #13803

* move height to constant variable in cost file

* fixed DI cypress

* fixed failing unit test
This commit is contained in:
Shailesh Parmar 2023-11-10 13:53:47 +05:30 committed by GitHub
parent 210f7d2f4b
commit cefe22247b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 848 additions and 600 deletions

View File

@ -111,7 +111,7 @@ describe('Data Insight feature', () => {
interceptURL('GET', '/api/v1/apps?limit=*', 'apps');
interceptURL(
'GET',
'/api/v1/apps/name/DataInsightsApplication?fields=owner,pipelines',
'/api/v1/apps/name/DataInsightsApplication?*',
'dataInsightsApplication'
);
interceptURL(
@ -128,7 +128,7 @@ describe('Data Insight feature', () => {
cy.get('[data-menu-id*="integrations.apps"]').scrollIntoView().click();
verifyResponseStatusCode('@apps', 200);
cy.get(
'[data-testid="data-insights-card"] [data-testid="config-btn"]'
'[data-testid="data-insights-application-card"] [data-testid="config-btn"]'
).click();
verifyResponseStatusCode('@dataInsightsApplication', 200);
cy.get('[data-testid="deploy-button"]').click();

View File

@ -46,7 +46,7 @@ describe('Data Insight settings page should work properly', () => {
'triggerPipeline'
);
cy.get(
'[data-testid="data-insights-card"] [data-testid="config-btn"]'
'[data-testid="data-insights-application-card"] [data-testid="config-btn"]'
).click();
verifyResponseStatusCode('@getDataInsightDetails', 200);
cy.get('[data-testid="deploy-button"]').click();
@ -67,7 +67,7 @@ describe('Data Insight settings page should work properly', () => {
);
interceptURL('PATCH', '/api/v1/apps/*', 'updateApplication');
cy.get(
'[data-testid="data-insights-card"] [data-testid="config-btn"]'
'[data-testid="data-insights-application-card"] [data-testid="config-btn"]'
).click();
verifyResponseStatusCode('@getDataInsightDetails', 200);
cy.get('[data-testid="edit-button"]').click();
@ -93,7 +93,7 @@ describe('Data Insight settings page should work properly', () => {
'deleteApplication'
);
cy.get(
'[data-testid="data-insights-card"] [data-testid="config-btn"]'
'[data-testid="data-insights-application-card"] [data-testid="config-btn"]'
).click();
verifyResponseStatusCode('@getDataInsightDetails', 200);
cy.get('[data-testid="manage-button"]').click();
@ -101,16 +101,18 @@ describe('Data Insight settings page should work properly', () => {
cy.get('[data-testid="save-button"]').click();
verifyResponseStatusCode('@deleteApplication', 200);
verifyResponseStatusCode('@getApplications', 200);
cy.get('[data-testid="data-insights-card"]').should('not.exist');
cy.get('[data-testid="data-insights-application-card"]').should(
'not.exist'
);
});
it('Install application', () => {
interceptURL('GET', '/api/v1/apps/marketplace?limit=*', 'getMarketPlace');
interceptURL('POST', '/api/v1/apps/install', 'installApplication');
interceptURL('POST', '/api/v1/apps', 'installApplication');
cy.get('[data-testid="add-application"]').click();
verifyResponseStatusCode('@getMarketPlace', 200);
cy.get(
'[data-testid="data-insights-card"] [data-testid="config-btn"]'
'[data-testid="data-insights-application-card"] [data-testid="config-btn"]'
).click();
cy.get('[data-testid="install-application"]').click();
cy.get('[data-testid="save-button"]').click();
@ -119,6 +121,8 @@ describe('Data Insight settings page should work properly', () => {
cy.get('[data-testid="deploy-button"]').click();
verifyResponseStatusCode('@installApplication', 201);
verifyResponseStatusCode('@getApplications', 200);
cy.get('[data-testid="data-insights-card"]').should('be.visible');
cy.get('[data-testid="data-insights-application-card"]').should(
'be.visible'
);
});
});

View File

@ -39,7 +39,7 @@ const ApplicationCard = ({
className,
'application-card card-body-border-none'
)}
data-testid={`${kebabCase(title)}-card`}>
data-testid={`${kebabCase(appName)}-card`}>
<div className="d-flex items-center gap-3">
<div className="application-logo">
<AppLogo appName={appName} />

View File

@ -32,6 +32,7 @@ import {
BAR_CHART_MARGIN,
DATA_INSIGHT_GRAPH_COLORS,
DI_STRUCTURE,
GRAPH_HEIGHT,
} from '../../constants/DataInsight.constants';
import { DataReportIndex } from '../../generated/dataInsight/dataInsightChart';
import { DataInsightChartType } from '../../generated/dataInsight/dataInsightChartResult';
@ -106,7 +107,7 @@ const DailyActiveUsersChart: FC<Props> = ({ chartFilter, selectedDays }) => {
{dailyActiveUsers.length ? (
<Row gutter={DI_STRUCTURE.rowContainerGutter}>
<Col span={DI_STRUCTURE.leftContainerSpan}>
<ResponsiveContainer debounce={1} minHeight={400}>
<ResponsiveContainer debounce={1} height={GRAPH_HEIGHT}>
<LineChart data={data} margin={BAR_CHART_MARGIN}>
<CartesianGrid
stroke={GRAPH_BACKGROUND_COLOR}

View File

@ -26,6 +26,10 @@
.ant-card-head {
border-bottom: 1px solid @border-color;
}
.custom-data-insight-tooltip-container {
max-height: 250px;
overflow-y: auto;
}
}
.ant-card-body {
padding: 16px 16px;

View File

@ -39,6 +39,7 @@ jest.mock('../../utils/DataInsightUtils', () => ({
getGraphDataByEntityType: jest
.fn()
.mockImplementation(() => DUMMY_GRAPH_DATA),
sortEntityByValue: jest.fn().mockImplementation((entities) => entities),
}));
jest.mock('./EntitySummaryProgressBar.component', () => {
return jest.fn().mockImplementation(({ label, entity }) => (
@ -58,18 +59,20 @@ jest.mock('react-i18next', () => ({
describe('Test DescriptionInsight Component', () => {
it('Should render the graph', async () => {
await act(async () => {
const { container } = render(<DescriptionInsight {...mockProps} />);
const card = screen.getByTestId('entity-description-percentage-card');
const graph = queryByAttribute(
'id',
container,
`${mockProps.dataInsightChartName}-graph`
);
expect(card).toBeInTheDocument();
expect(graph).toBeInTheDocument();
render(<DescriptionInsight {...mockProps} />);
});
const card = await screen.findByTestId(
'entity-description-percentage-card'
);
const graph = queryByAttribute(
'id',
card,
`${mockProps.dataInsightChartName}-graph`
);
expect(card).toBeInTheDocument();
expect(graph).toBeInTheDocument();
});
it('Should render the graph and progress bar even if one entity dont have values', async () => {
@ -77,21 +80,23 @@ describe('Test DescriptionInsight Component', () => {
() => DUMMY_GRAPH_DATA_WITH_MISSING_ENTITY
);
await act(async () => {
const { container } = render(<DescriptionInsight {...mockProps} />);
const card = screen.getByTestId('entity-description-percentage-card');
const graph = queryByAttribute(
'id',
container,
`${mockProps.dataInsightChartName}-graph`
);
const missingEntityValue = await screen.findByTestId('Table');
expect(card).toBeInTheDocument();
expect(graph).toBeInTheDocument();
expect(missingEntityValue).toBeInTheDocument();
expect(missingEntityValue.textContent).toBe('0');
render(<DescriptionInsight {...mockProps} />);
});
const card = await screen.findByTestId(
'entity-description-percentage-card'
);
const graph = queryByAttribute(
'id',
card,
`${mockProps.dataInsightChartName}-graph`
);
const missingEntityValue = await screen.findByTestId('Table');
expect(card).toBeInTheDocument();
expect(graph).toBeInTheDocument();
expect(missingEntityValue).toBeInTheDocument();
expect(missingEntityValue.textContent).toBe('0');
});
it('Should fetch data based on dataInsightChartName props', async () => {

View File

@ -11,15 +11,13 @@
* limitations under the License.
*/
import { Card, Col, Row } from 'antd';
import { Button, Card, Col, Row } from 'antd';
import { AxiosError } from 'axios';
import { isEmpty, round, uniqueId } from 'lodash';
import { includes, isEmpty, round, toLower } from 'lodash';
import React, { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
CartesianGrid,
Legend,
LegendProps,
Line,
LineChart,
ResponsiveContainer,
@ -36,6 +34,7 @@ import {
import {
BAR_CHART_MARGIN,
DI_STRUCTURE,
GRAPH_HEIGHT,
TOTAL_ENTITY_CHART_COLOR,
} from '../../constants/DataInsight.constants';
import { DataReportIndex } from '../../generated/dataInsight/dataInsightChart';
@ -53,9 +52,11 @@ import {
import {
CustomTooltip,
getGraphDataByEntityType,
renderLegend,
getRandomHexColor,
sortEntityByValue,
} from '../../utils/DataInsightUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import Searchbar from '../common/searchbar/Searchbar';
import './DataInsightDetail.less';
import DataInsightProgressBar from './DataInsightProgressBar';
import { EmptyGraphPlaceholder } from './EmptyGraphPlaceholder';
@ -79,9 +80,10 @@ const DescriptionInsight: FC<Props> = ({
const [totalEntitiesDescriptionByType, setTotalEntitiesDescriptionByType] =
useState<DataInsightChartResult>();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [activeKeys, setActiveKeys] = useState<string[]>([]);
const [activeMouseHoverKey, setActiveMouseHoverKey] = useState('');
const [searchEntityKeyWord, setSearchEntityKeyWord] = useState('');
const {
data,
@ -97,6 +99,18 @@ const DescriptionInsight: FC<Props> = ({
);
}, [totalEntitiesDescriptionByType]);
const sortedEntitiesByValue = useMemo(() => {
return sortEntityByValue(entities, latestData);
}, [entities, latestData]);
const rightSideEntityList = useMemo(
() =>
sortedEntitiesByValue.filter((entity) =>
includes(toLower(entity), toLower(searchEntityKeyWord))
),
[sortedEntitiesByValue, searchEntityKeyWord]
);
const { t } = useTranslation();
const targetValue = useMemo(() => {
@ -125,16 +139,16 @@ const DescriptionInsight: FC<Props> = ({
}
};
const handleLegendClick: LegendProps['onClick'] = (event) => {
const handleLegendClick = (entity: string) => {
setActiveKeys((prevActiveKeys) =>
updateActiveChartFilter(event.dataKey, prevActiveKeys)
updateActiveChartFilter(entity, prevActiveKeys)
);
};
const handleLegendMouseEnter: LegendProps['onMouseEnter'] = (event) => {
setActiveMouseHoverKey(event.dataKey);
const handleLegendMouseEnter = (entity: string) => {
setActiveMouseHoverKey(entity);
};
const handleLegendMouseLeave: LegendProps['onMouseLeave'] = () => {
const handleLegendMouseLeave = () => {
setActiveMouseHoverKey('');
};
@ -142,111 +156,137 @@ const DescriptionInsight: FC<Props> = ({
fetchTotalEntitiesDescriptionByType();
}, [chartFilter]);
if (isLoading || data.length === 0) {
return (
<Card
className="data-insight-card"
loading={isLoading}
title={
<PageHeader
data={{
header,
subHeader: t('message.field-insight', {
field: t('label.description-lowercase'),
}),
}}
/>
}>
<EmptyGraphPlaceholder />
</Card>
);
}
return (
<Card
className="data-insight-card"
data-testid="entity-description-percentage-card"
id={dataInsightChartName}
loading={isLoading}
title={
<PageHeader
data={{
header,
subHeader: t('message.field-insight', {
field: t('label.description-lowercase'),
}),
}}
/>
}>
{data.length ? (
<Row gutter={DI_STRUCTURE.rowContainerGutter}>
<Col span={DI_STRUCTURE.leftContainerSpan}>
<ResponsiveContainer
debounce={1}
id={`${dataInsightChartName}-graph`}
minHeight={400}>
<LineChart data={data} margin={BAR_CHART_MARGIN}>
<CartesianGrid
stroke={GRAPH_BACKGROUND_COLOR}
vertical={false}
/>
<XAxis dataKey="timestamp" />
<YAxis
tickFormatter={(value: number) =>
axisTickFormatter(value, '%')
id={dataInsightChartName}>
<Row gutter={DI_STRUCTURE.rowContainerGutter}>
<Col span={DI_STRUCTURE.leftContainerSpan}>
<PageHeader
data={{
header,
subHeader: t('message.field-insight', {
field: t('label.description-lowercase'),
}),
}}
/>
<ResponsiveContainer
className="m-t-lg"
debounce={1}
height={GRAPH_HEIGHT}
id={`${dataInsightChartName}-graph`}>
<LineChart data={data} margin={BAR_CHART_MARGIN}>
<CartesianGrid stroke={GRAPH_BACKGROUND_COLOR} vertical={false} />
<XAxis dataKey="timestamp" />
<YAxis
tickFormatter={(value: number) => axisTickFormatter(value, '%')}
/>
<Tooltip
content={<CustomTooltip isPercentage />}
wrapperStyle={{ pointerEvents: 'auto' }}
/>
{entities.map((entity, i) => (
<Line
dataKey={entity}
hide={
activeKeys.length && entity !== activeMouseHoverKey
? !activeKeys.includes(entity)
: false
}
/>
<Tooltip content={<CustomTooltip isPercentage />} />
<Legend
align="left"
content={(props) =>
renderLegend(props as LegendProps, activeKeys)
key={entity}
stroke={TOTAL_ENTITY_CHART_COLOR[i] ?? getRandomHexColor()}
strokeOpacity={
isEmpty(activeMouseHoverKey) ||
entity === activeMouseHoverKey
? DEFAULT_CHART_OPACITY
: HOVER_CHART_OPACITY
}
layout="horizontal"
verticalAlign="top"
wrapperStyle={{ left: '0px', top: '0px' }}
onClick={handleLegendClick}
onMouseEnter={handleLegendMouseEnter}
onMouseLeave={handleLegendMouseLeave}
/>
{entities.map((entity, i) => (
<Line
dataKey={entity}
hide={
activeKeys.length && entity !== activeMouseHoverKey
? !activeKeys.includes(entity)
: false
}
key={entity}
stroke={TOTAL_ENTITY_CHART_COLOR[i]}
strokeOpacity={
isEmpty(activeMouseHoverKey) ||
entity === activeMouseHoverKey
? DEFAULT_CHART_OPACITY
: HOVER_CHART_OPACITY
}
type="monotone"
/>
))}
</LineChart>
</ResponsiveContainer>
</Col>
<Col span={DI_STRUCTURE.rightContainerSpan}>
<Row gutter={DI_STRUCTURE.rightRowGutter}>
<Col span={24}>
<DataInsightProgressBar
changeInValue={relativePercentage}
className="m-b-md"
duration={selectedDays}
label={`${t('label.completed-entity', {
entity: t('label.description'),
})}${isPercentageGraph ? ' %' : ''}`}
progress={Number(total)}
suffix={isPercentageGraph ? '%' : ''}
target={targetValue}
type="monotone"
/>
))}
</LineChart>
</ResponsiveContainer>
</Col>
<Col span={DI_STRUCTURE.rightContainerSpan}>
<Row gutter={[8, 16]}>
<Col span={24}>
<DataInsightProgressBar
changeInValue={relativePercentage}
duration={selectedDays}
label={`${t('label.completed-entity', {
entity: t('label.description'),
})}${isPercentageGraph ? ' %' : ''}`}
progress={Number(total)}
suffix={isPercentageGraph ? '%' : ''}
target={targetValue}
/>
</Col>
<Col span={24}>
<Searchbar
removeMargin
searchValue={searchEntityKeyWord}
onSearch={setSearchEntityKeyWord}
/>
</Col>
<Col className="chart-card-right-panel-container" span={24}>
<Row gutter={[8, 8]}>
{rightSideEntityList.map((entity, i) => {
return (
<Col
className="entity-summary-container"
key={entity}
span={24}
onClick={() => handleLegendClick(entity)}
onMouseEnter={() => handleLegendMouseEnter(entity)}
onMouseLeave={handleLegendMouseLeave}>
<EntitySummaryProgressBar
entity={entity}
isActive={
activeKeys.length ? activeKeys.includes(entity) : true
}
label={`${round(latestData[entity] ?? 0, 2)}${
isPercentageGraph ? '%' : ''
}`}
latestData={latestData}
progress={latestData[entity]}
strokeColor={TOTAL_ENTITY_CHART_COLOR[i]}
/>
</Col>
);
})}
</Row>
</Col>
{activeKeys.length > 0 && (
<Col className="flex justify-end" span={24}>
<Button type="link" onClick={() => setActiveKeys([])}>
{t('label.clear')}
</Button>
</Col>
{entities.map((entity, i) => {
return (
<Col key={uniqueId()} span={24}>
<EntitySummaryProgressBar
entity={entity}
label={`${round(latestData[entity] ?? 0, 2)}${
isPercentageGraph ? '%' : ''
}`}
latestData={latestData}
progress={latestData[entity]}
strokeColor={TOTAL_ENTITY_CHART_COLOR[i]}
/>
</Col>
);
})}
</Row>
</Col>
</Row>
) : (
<EmptyGraphPlaceholder />
)}
)}
</Row>
</Col>
</Row>
</Card>
);
};

View File

@ -11,11 +11,14 @@
* limitations under the License.
*/
import { Col, Progress, Row, Typography } from 'antd';
import classNames from 'classnames';
import { round } from 'lodash';
import React, { ReactNode } from 'react';
import { GRAYED_OUT_COLOR } from '../../constants/constants';
type EntitySummaryProgressBarProps = {
pluralize?: boolean;
isActive?: boolean;
progress: number;
entity: string;
latestData: Record<string, number>;
@ -24,6 +27,7 @@ type EntitySummaryProgressBarProps = {
};
const EntitySummaryProgressBar = ({
isActive = true,
pluralize = true,
entity,
latestData,
@ -36,16 +40,24 @@ const EntitySummaryProgressBar = ({
};
return (
<Row className="m-b-xs" data-testid="entity-summary-container">
<Row
className={classNames({
'non-active-details': !isActive,
})}
data-testid="entity-summary-container">
<Col
className="d-flex justify-between items-center text-xs"
md={12}
sm={24}>
<Typography.Paragraph className="m-b-0" data-testid="entity-name">
<Typography.Paragraph
className="m-b-0 entity-summary-name break-all"
data-testid="entity-name">
{pluralize ? pluralizeName(entity) : entity}
</Typography.Paragraph>
<Typography.Paragraph className="m-b-0" data-testid="entity-value">
<Typography.Paragraph
className="m-b-0 entity-summary-value"
data-testid="entity-value">
{label ?? round(latestData[entity] || 0, 2)}
</Typography.Paragraph>
</Col>
@ -56,7 +68,7 @@ const EntitySummaryProgressBar = ({
percent={progress}
showInfo={false}
size="small"
strokeColor={strokeColor}
strokeColor={isActive ? strokeColor : GRAYED_OUT_COLOR}
/>
</Col>
</Row>

View File

@ -41,6 +41,7 @@ import {
BAR_CHART_MARGIN,
DATA_INSIGHT_GRAPH_COLORS,
DI_STRUCTURE,
GRAPH_HEIGHT,
} from '../../constants/DataInsight.constants';
import { ERROR_PLACEHOLDER_TYPE, SIZE } from '../../enums/common.enum';
import {
@ -214,8 +215,8 @@ const KPIChart: FC<Props> = ({
<Col span={DI_STRUCTURE.leftContainerSpan}>
<ResponsiveContainer
debounce={1}
id="kpi-chart"
minHeight={400}>
height={GRAPH_HEIGHT}
id="kpi-chart">
<LineChart data={graphData} margin={BAR_CHART_MARGIN}>
<CartesianGrid
stroke={GRAPH_BACKGROUND_COLOR}

View File

@ -37,6 +37,7 @@ jest.mock('../../utils/DataInsightUtils', () => ({
getGraphDataByEntityType: jest
.fn()
.mockImplementation(() => DUMMY_GRAPH_DATA),
sortEntityByValue: jest.fn().mockImplementation((entities) => entities),
}));
jest.mock('./EntitySummaryProgressBar.component', () => {
return jest.fn().mockImplementation(({ label, entity }) => (
@ -59,18 +60,18 @@ jest.mock('react-i18next', () => ({
describe('Test DescriptionInsight Component', () => {
it('Should render the graph', async () => {
await act(async () => {
const { container } = render(<OwnerInsight {...mockProps} />);
const card = screen.getByTestId('entity-summary-card-percentage');
const graph = queryByAttribute(
'id',
container,
`${mockProps.dataInsightChartName}-graph`
);
expect(card).toBeInTheDocument();
expect(graph).toBeInTheDocument();
render(<OwnerInsight {...mockProps} />);
});
const card = await screen.findByTestId('entity-summary-card-percentage');
const graph = queryByAttribute(
'id',
card,
`${mockProps.dataInsightChartName}-graph`
);
expect(card).toBeInTheDocument();
expect(graph).toBeInTheDocument();
});
it('Should render the graph and progress bar even if one entity dont have values', async () => {
@ -78,21 +79,21 @@ describe('Test DescriptionInsight Component', () => {
() => DUMMY_GRAPH_DATA_WITH_MISSING_ENTITY
);
await act(async () => {
const { container } = render(<OwnerInsight {...mockProps} />);
const card = screen.getByTestId('entity-summary-card-percentage');
const graph = queryByAttribute(
'id',
container,
`${mockProps.dataInsightChartName}-graph`
);
const missingEntityValue = await screen.findByTestId('Table');
expect(card).toBeInTheDocument();
expect(graph).toBeInTheDocument();
expect(missingEntityValue).toBeInTheDocument();
expect(missingEntityValue.textContent).toBe('0');
render(<OwnerInsight {...mockProps} />);
});
const card = await screen.findByTestId('entity-summary-card-percentage');
const graph = queryByAttribute(
'id',
card,
`${mockProps.dataInsightChartName}-graph`
);
const missingEntityValue = await screen.findByTestId('Table');
expect(card).toBeInTheDocument();
expect(graph).toBeInTheDocument();
expect(missingEntityValue).toBeInTheDocument();
expect(missingEntityValue.textContent).toBe('0');
});
it('Should fetch data based on dataInsightChartName props', async () => {

View File

@ -11,15 +11,13 @@
* limitations under the License.
*/
import { Card, Col, Row } from 'antd';
import { Button, Card, Col, Row } from 'antd';
import { AxiosError } from 'axios';
import { isEmpty, round } from 'lodash';
import { includes, isEmpty, round, toLower } from 'lodash';
import React, { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
CartesianGrid,
Legend,
LegendProps,
Line,
LineChart,
ResponsiveContainer,
@ -36,6 +34,7 @@ import {
import {
BAR_CHART_MARGIN,
DI_STRUCTURE,
GRAPH_HEIGHT,
TOTAL_ENTITY_CHART_COLOR,
} from '../../constants/DataInsight.constants';
import { DataReportIndex } from '../../generated/dataInsight/dataInsightChart';
@ -53,9 +52,11 @@ import {
import {
CustomTooltip,
getGraphDataByEntityType,
renderLegend,
getRandomHexColor,
sortEntityByValue,
} from '../../utils/DataInsightUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import Searchbar from '../common/searchbar/Searchbar';
import './DataInsightDetail.less';
import DataInsightProgressBar from './DataInsightProgressBar';
import { EmptyGraphPlaceholder } from './EmptyGraphPlaceholder';
@ -79,9 +80,10 @@ const OwnerInsight: FC<Props> = ({
const [totalEntitiesOwnerByType, setTotalEntitiesOwnerByType] =
useState<DataInsightChartResult>();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [activeKeys, setActiveKeys] = useState<string[]>([]);
const [activeMouseHoverKey, setActiveMouseHoverKey] = useState('');
const [searchEntityKeyWord, setSearchEntityKeyWord] = useState('');
const {
data,
@ -105,6 +107,18 @@ const OwnerInsight: FC<Props> = ({
return undefined;
}, [kpi]);
const sortedEntitiesByValue = useMemo(() => {
return sortEntityByValue(entities, latestData);
}, [entities, latestData]);
const rightSideEntityList = useMemo(
() =>
sortedEntitiesByValue.filter((entity) =>
includes(toLower(entity), toLower(searchEntityKeyWord))
),
[sortedEntitiesByValue, searchEntityKeyWord]
);
const { t } = useTranslation();
const fetchTotalEntitiesOwnerByType = async () => {
@ -125,15 +139,16 @@ const OwnerInsight: FC<Props> = ({
}
};
const handleLegendClick: LegendProps['onClick'] = (event) => {
const handleLegendClick = (entity: string) => {
setActiveKeys((prevActiveKeys) =>
updateActiveChartFilter(event.dataKey, prevActiveKeys)
updateActiveChartFilter(entity, prevActiveKeys)
);
};
const handleLegendMouseEnter: LegendProps['onMouseEnter'] = (event) => {
setActiveMouseHoverKey(event.dataKey);
const handleLegendMouseEnter = (entity: string) => {
setActiveMouseHoverKey(entity);
};
const handleLegendMouseLeave: LegendProps['onMouseLeave'] = () => {
const handleLegendMouseLeave = () => {
setActiveMouseHoverKey('');
};
@ -141,112 +156,139 @@ const OwnerInsight: FC<Props> = ({
fetchTotalEntitiesOwnerByType();
}, [chartFilter]);
if (isLoading || data.length === 0) {
return (
<Card
className="data-insight-card"
loading={isLoading}
title={
<PageHeader
data={{
header,
subHeader: t('message.field-insight', {
field: t('label.owner'),
}),
}}
/>
}>
<EmptyGraphPlaceholder />
</Card>
);
}
return (
<Card
className="data-insight-card"
data-testid="entity-summary-card-percentage"
id={dataInsightChartName}
loading={isLoading}
title={
<PageHeader
data={{
header,
subHeader: t('message.field-insight', {
field: t('label.owner'),
}),
}}
/>
}>
{data.length ? (
<Row gutter={DI_STRUCTURE.rowContainerGutter}>
<Col span={DI_STRUCTURE.leftContainerSpan}>
<ResponsiveContainer
debounce={1}
id={`${dataInsightChartName}-graph`}
minHeight={400}>
<LineChart data={data} margin={BAR_CHART_MARGIN}>
<CartesianGrid
stroke={GRAPH_BACKGROUND_COLOR}
vertical={false}
/>
<XAxis dataKey="timestamp" />
<YAxis
tickFormatter={(value: number) =>
axisTickFormatter(value, '%')
loading={isLoading}>
<Row gutter={DI_STRUCTURE.rowContainerGutter}>
<Col span={DI_STRUCTURE.leftContainerSpan}>
<PageHeader
data={{
header,
subHeader: t('message.field-insight', {
field: t('label.owner'),
}),
}}
/>
<ResponsiveContainer
className="m-t-lg"
debounce={1}
height={GRAPH_HEIGHT}
id={`${dataInsightChartName}-graph`}>
<LineChart data={data} margin={BAR_CHART_MARGIN}>
<CartesianGrid stroke={GRAPH_BACKGROUND_COLOR} vertical={false} />
<XAxis dataKey="timestamp" />
<YAxis
tickFormatter={(value: number) => axisTickFormatter(value, '%')}
/>
<Tooltip
content={<CustomTooltip isPercentage />}
wrapperStyle={{ pointerEvents: 'auto' }}
/>
{entities.map((entity, i) => (
<Line
dataKey={entity}
hide={
activeKeys.length && entity !== activeMouseHoverKey
? !activeKeys.includes(entity)
: false
}
/>
<Tooltip content={<CustomTooltip isPercentage />} />
<Legend
align="left"
content={(props) =>
renderLegend(props as LegendProps, activeKeys)
key={entity}
stroke={TOTAL_ENTITY_CHART_COLOR[i] ?? getRandomHexColor()}
strokeOpacity={
isEmpty(activeMouseHoverKey) ||
entity === activeMouseHoverKey
? DEFAULT_CHART_OPACITY
: HOVER_CHART_OPACITY
}
layout="horizontal"
verticalAlign="top"
wrapperStyle={{ left: '0px', top: '0px' }}
onClick={handleLegendClick}
onMouseEnter={handleLegendMouseEnter}
onMouseLeave={handleLegendMouseLeave}
type="monotone"
/>
{entities.map((entity, i) => (
<Line
dataKey={entity}
hide={
activeKeys.length && entity !== activeMouseHoverKey
? !activeKeys.includes(entity)
: false
}
key={entity}
stroke={TOTAL_ENTITY_CHART_COLOR[i]}
strokeOpacity={
isEmpty(activeMouseHoverKey) ||
entity === activeMouseHoverKey
? DEFAULT_CHART_OPACITY
: HOVER_CHART_OPACITY
}
type="monotone"
/>
))}
</LineChart>
</ResponsiveContainer>
</Col>
<Col span={DI_STRUCTURE.rightContainerSpan}>
<Row gutter={DI_STRUCTURE.rightRowGutter}>
<Col span={24}>
<DataInsightProgressBar
changeInValue={relativePercentage}
className="m-b-md"
duration={selectedDays}
label={`${t('label.assigned-entity', {
entity: t('label.owner'),
})}
))}
</LineChart>
</ResponsiveContainer>
</Col>
<Col span={DI_STRUCTURE.rightContainerSpan}>
<Row gutter={[8, 16]}>
<Col span={24}>
<DataInsightProgressBar
changeInValue={relativePercentage}
duration={selectedDays}
label={`${t('label.assigned-entity', {
entity: t('label.owner'),
})}
${isPercentageGraph ? ' %' : ''}`}
progress={Number(total)}
suffix={isPercentageGraph ? '%' : ''}
target={targetValue}
/>
progress={Number(total)}
suffix={isPercentageGraph ? '%' : ''}
target={targetValue}
/>
</Col>
<Col span={24}>
<Searchbar
removeMargin
searchValue={searchEntityKeyWord}
onSearch={setSearchEntityKeyWord}
/>
</Col>
<Col className="chart-card-right-panel-container" span={24}>
<Row gutter={[8, 8]}>
{rightSideEntityList.map((entity, i) => {
return (
<Col
className="entity-summary-container"
key={entity}
span={24}
onClick={() => handleLegendClick(entity)}
onMouseEnter={() => handleLegendMouseEnter(entity)}
onMouseLeave={handleLegendMouseLeave}>
<EntitySummaryProgressBar
entity={entity}
isActive={
activeKeys.length ? activeKeys.includes(entity) : true
}
label={`${round(latestData[entity] || 0, 2)}${
isPercentageGraph ? '%' : ''
}`}
latestData={latestData}
progress={latestData[entity]}
strokeColor={TOTAL_ENTITY_CHART_COLOR[i]}
/>
</Col>
);
})}
</Row>
</Col>
{activeKeys.length > 0 && (
<Col className="flex justify-end" span={24}>
<Button type="link" onClick={() => setActiveKeys([])}>
{t('label.clear')}
</Button>
</Col>
{entities.map((entity, i) => {
return (
<Col key={entity} span={24}>
<EntitySummaryProgressBar
entity={entity}
label={`${round(latestData[entity] || 0, 2)}${
isPercentageGraph ? '%' : ''
}`}
latestData={latestData}
progress={latestData[entity]}
strokeColor={TOTAL_ENTITY_CHART_COLOR[i]}
/>
</Col>
);
})}
</Row>
</Col>
</Row>
) : (
<EmptyGraphPlaceholder />
)}
)}
</Row>
</Col>
</Row>
</Card>
);
};

View File

@ -18,8 +18,6 @@ import React, { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
CartesianGrid,
Legend,
LegendProps,
Line,
LineChart,
ResponsiveContainer,
@ -36,6 +34,7 @@ import {
import {
BAR_CHART_MARGIN,
DI_STRUCTURE,
GRAPH_HEIGHT,
TOTAL_ENTITY_CHART_COLOR,
} from '../../constants/DataInsight.constants';
import { DataReportIndex } from '../../generated/dataInsight/dataInsightChart';
@ -43,11 +42,10 @@ import { DataInsightChartType } from '../../generated/dataInsight/dataInsightCha
import { PageViewsByEntities } from '../../generated/dataInsight/type/pageViewsByEntities';
import { ChartFilter } from '../../interface/data-insight.interface';
import { getAggregateChartData } from '../../rest/DataInsightAPI';
import { updateActiveChartFilter } from '../../utils/ChartUtils';
import {
CustomTooltip,
getGraphDataByEntityType,
renderLegend,
sortEntityByValue,
} from '../../utils/DataInsightUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import './DataInsightDetail.less';
@ -63,7 +61,7 @@ const PageViewsByEntitiesChart: FC<Props> = ({ chartFilter, selectedDays }) => {
const [pageViewsByEntities, setPageViewsByEntities] =
useState<PageViewsByEntities[]>();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [activeKeys, setActiveKeys] = useState<string[]>([]);
const [activeMouseHoverKey, setActiveMouseHoverKey] = useState('');
@ -74,6 +72,9 @@ const PageViewsByEntitiesChart: FC<Props> = ({ chartFilter, selectedDays }) => {
DataInsightChartType.PageViewsByEntities
);
}, [pageViewsByEntities]);
const sortedEntitiesByValue = useMemo(() => {
return sortEntityByValue(entities, latestData);
}, [entities, latestData]);
const { t } = useTranslation();
@ -95,97 +96,89 @@ const PageViewsByEntitiesChart: FC<Props> = ({ chartFilter, selectedDays }) => {
}
};
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(() => {
fetchPageViewsByEntities();
}, [chartFilter]);
if (isLoading || data.length === 0) {
return (
<Card
className="data-insight-card"
loading={isLoading}
title={
<PageHeader
data={{
header: t('label.page-views-by-data-asset-plural'),
subHeader: t('message.data-insight-page-views'),
}}
/>
}>
<EmptyGraphPlaceholder />
</Card>
);
}
return (
<Card
className="data-insight-card"
data-testid="entity-page-views-card"
id={DataInsightChartType.PageViewsByEntities}
loading={isLoading}
title={
<PageHeader
data={{
header: t('label.page-views-by-data-asset-plural'),
subHeader: t('message.data-insight-page-views'),
}}
/>
}>
{data.length ? (
<Row gutter={DI_STRUCTURE.rowContainerGutter}>
<Col span={DI_STRUCTURE.leftContainerSpan}>
<ResponsiveContainer debounce={1} minHeight={400}>
<LineChart data={data} margin={BAR_CHART_MARGIN}>
<CartesianGrid
stroke={GRAPH_BACKGROUND_COLOR}
vertical={false}
/>
<XAxis dataKey="timestamp" />
<YAxis />
<Tooltip content={<CustomTooltip />} />
<Legend
align="left"
content={(props) =>
renderLegend(props as LegendProps, activeKeys)
loading={isLoading}>
<Row gutter={DI_STRUCTURE.rowContainerGutter}>
<Col span={DI_STRUCTURE.leftContainerSpan}>
<PageHeader
data={{
header: t('label.page-views-by-data-asset-plural'),
subHeader: t('message.data-insight-page-views'),
}}
/>
<ResponsiveContainer debounce={1} height={GRAPH_HEIGHT}>
<LineChart data={data} margin={BAR_CHART_MARGIN}>
<CartesianGrid stroke={GRAPH_BACKGROUND_COLOR} vertical={false} />
<XAxis dataKey="timestamp" />
<YAxis />
<Tooltip
content={<CustomTooltip />}
wrapperStyle={{ pointerEvents: 'auto' }}
/>
{entities.map((entity, i) => (
<Line
dataKey={entity}
hide={
activeKeys.length && entity !== activeMouseHoverKey
? !activeKeys.includes(entity)
: false
}
layout="horizontal"
verticalAlign="top"
wrapperStyle={{ left: '0px', top: '0px' }}
onClick={handleLegendClick}
onMouseEnter={handleLegendMouseEnter}
onMouseLeave={handleLegendMouseLeave}
key={entity}
stroke={TOTAL_ENTITY_CHART_COLOR[i]}
strokeOpacity={
isEmpty(activeMouseHoverKey) ||
entity === activeMouseHoverKey
? DEFAULT_CHART_OPACITY
: HOVER_CHART_OPACITY
}
type="monotone"
/>
{entities.map((entity, i) => (
<Line
dataKey={entity}
hide={
activeKeys.length && entity !== activeMouseHoverKey
? !activeKeys.includes(entity)
: false
}
key={entity}
stroke={TOTAL_ENTITY_CHART_COLOR[i]}
strokeOpacity={
isEmpty(activeMouseHoverKey) ||
entity === activeMouseHoverKey
? DEFAULT_CHART_OPACITY
: HOVER_CHART_OPACITY
}
type="monotone"
/>
))}
</LineChart>
</ResponsiveContainer>
</Col>
<Col span={DI_STRUCTURE.rightContainerSpan}>
<TotalEntityInsightSummary
entities={entities}
gutter={DI_STRUCTURE.rightRowGutter}
latestData={latestData}
relativePercentage={relativePercentage}
selectedDays={selectedDays}
total={total}
/>
</Col>
</Row>
) : (
<EmptyGraphPlaceholder />
)}
))}
</LineChart>
</ResponsiveContainer>
</Col>
<Col span={DI_STRUCTURE.rightContainerSpan}>
<TotalEntityInsightSummary
allowFilter
activeKeys={activeKeys}
entities={sortedEntitiesByValue}
gutter={DI_STRUCTURE.rightRowGutter}
latestData={latestData}
relativePercentage={relativePercentage}
selectedDays={selectedDays}
total={total}
onActiveKeyMouseHover={(entity) => setActiveMouseHoverKey(entity)}
onActiveKeysUpdate={(entities) => setActiveKeys(entities)}
/>
</Col>
</Row>
</Card>
);
};

View File

@ -11,15 +11,13 @@
* limitations under the License.
*/
import { Card, Col, Row } from 'antd';
import { Button, Card, Col, Row } from 'antd';
import { AxiosError } from 'axios';
import { isEmpty, round } from 'lodash';
import { includes, isEmpty, round, toLower } from 'lodash';
import React, { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
CartesianGrid,
Legend,
LegendProps,
Line,
LineChart,
ResponsiveContainer,
@ -36,6 +34,7 @@ import {
import {
BAR_CHART_MARGIN,
DI_STRUCTURE,
GRAPH_HEIGHT,
TOTAL_ENTITY_CHART_COLOR,
} from '../../constants/DataInsight.constants';
import { DataReportIndex } from '../../generated/dataInsight/dataInsightChart';
@ -53,10 +52,11 @@ import {
import {
CustomTooltip,
getGraphDataByTierType,
renderLegend,
sortEntityByValue,
} from '../../utils/DataInsightUtils';
import { getEntityName } from '../../utils/EntityUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import Searchbar from '../common/searchbar/Searchbar';
import './DataInsightDetail.less';
import DataInsightProgressBar from './DataInsightProgressBar';
import { EmptyGraphPlaceholder } from './EmptyGraphPlaceholder';
@ -75,10 +75,21 @@ const TierInsight: FC<Props> = ({ chartFilter, selectedDays, tierTags }) => {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [activeKeys, setActiveKeys] = useState<string[]>([]);
const [activeMouseHoverKey, setActiveMouseHoverKey] = useState('');
const [searchEntityKeyWord, setSearchEntityKeyWord] = useState('');
const { data, tiers, total, relativePercentage, latestData } = useMemo(() => {
return getGraphDataByTierType(totalEntitiesByTier?.data ?? []);
}, [totalEntitiesByTier]);
const sortedEntitiesByValue = useMemo(() => {
return sortEntityByValue(tiers, latestData);
}, [tiers, latestData]);
const rightSideEntityList = useMemo(
() =>
sortedEntitiesByValue.filter((entity) =>
includes(toLower(entity), toLower(searchEntityKeyWord))
),
[sortedEntitiesByValue, searchEntityKeyWord]
);
const { t } = useTranslation();
@ -109,15 +120,16 @@ const TierInsight: FC<Props> = ({ chartFilter, selectedDays, tierTags }) => {
}
};
const handleLegendClick: LegendProps['onClick'] = (event) => {
const handleLegendClick = (entity: string) => {
setActiveKeys((prevActiveKeys) =>
updateActiveChartFilter(event.dataKey, prevActiveKeys)
updateActiveChartFilter(entity, prevActiveKeys)
);
};
const handleLegendMouseEnter: LegendProps['onMouseEnter'] = (event) => {
setActiveMouseHoverKey(event.dataKey);
const handleLegendMouseEnter = (entity: string) => {
setActiveMouseHoverKey(entity);
};
const handleLegendMouseLeave: LegendProps['onMouseLeave'] = () => {
const handleLegendMouseLeave = () => {
setActiveMouseHoverKey('');
};
@ -127,107 +139,132 @@ const TierInsight: FC<Props> = ({ chartFilter, selectedDays, tierTags }) => {
}
}, [chartFilter, tierTags]);
if (isLoading || data.length === 0) {
return (
<Card
className="data-insight-card"
loading={isLoading}
title={
<PageHeader
data={{
header: t('label.data-insight-tier-summary'),
subHeader: t('message.field-insight', {
field: t('label.tier'),
}),
}}
/>
}>
<EmptyGraphPlaceholder />
</Card>
);
}
return (
<Card
className="data-insight-card"
data-testid="entity-summary-card-percentage"
id={DataInsightChartType.TotalEntitiesByTier}
loading={isLoading}
title={
<PageHeader
data={{
header: t('label.data-insight-tier-summary'),
subHeader: t('message.field-insight', {
field: t('label.tier'),
}),
}}
/>
}>
{data.length ? (
<Row gutter={DI_STRUCTURE.rowContainerGutter}>
<Col span={DI_STRUCTURE.leftContainerSpan}>
<ResponsiveContainer
debounce={1}
id={`${DataInsightChartType.TotalEntitiesByTier}-graph`}
minHeight={400}>
<LineChart data={data} margin={BAR_CHART_MARGIN}>
<CartesianGrid
stroke={GRAPH_BACKGROUND_COLOR}
vertical={false}
/>
<XAxis dataKey="timestamp" />
<YAxis
tickFormatter={(value) => axisTickFormatter(value, '%')}
/>
<Tooltip content={<CustomTooltip isPercentage isTier />} />
<Legend
align="left"
content={(props) =>
renderLegend(props as LegendProps, activeKeys)
loading={isLoading}>
<Row gutter={DI_STRUCTURE.rowContainerGutter}>
<Col span={DI_STRUCTURE.leftContainerSpan}>
<PageHeader
data={{
header: t('label.data-insight-tier-summary'),
subHeader: t('message.field-insight', {
field: t('label.tier'),
}),
}}
/>
<ResponsiveContainer
debounce={1}
height={GRAPH_HEIGHT}
id={`${DataInsightChartType.TotalEntitiesByTier}-graph`}>
<LineChart data={data} margin={BAR_CHART_MARGIN}>
<CartesianGrid stroke={GRAPH_BACKGROUND_COLOR} vertical={false} />
<XAxis dataKey="timestamp" />
<YAxis tickFormatter={(value) => axisTickFormatter(value, '%')} />
<Tooltip
content={<CustomTooltip isPercentage isTier />}
wrapperStyle={{ pointerEvents: 'auto' }}
/>
{tiers.map((tier, i) => (
<Line
dataKey={tier}
hide={
activeKeys.length && tier !== activeMouseHoverKey
? !activeKeys.includes(tier)
: false
}
layout="horizontal"
verticalAlign="top"
wrapperStyle={{ left: '0px', top: '0px' }}
onClick={handleLegendClick}
onMouseEnter={handleLegendMouseEnter}
onMouseLeave={handleLegendMouseLeave}
/>
{tiers.map((tier, i) => (
<Line
dataKey={tier}
hide={
activeKeys.length && tier !== activeMouseHoverKey
? !activeKeys.includes(tier)
: false
}
key={tier}
stroke={TOTAL_ENTITY_CHART_COLOR[i]}
strokeOpacity={
isEmpty(activeMouseHoverKey) ||
tier === activeMouseHoverKey
? DEFAULT_CHART_OPACITY
: HOVER_CHART_OPACITY
}
type="monotone"
/>
))}
</LineChart>
</ResponsiveContainer>
</Col>
<Col span={DI_STRUCTURE.rightContainerSpan}>
<Row gutter={DI_STRUCTURE.rightRowGutter}>
<Col span={24}>
<DataInsightProgressBar
changeInValue={relativePercentage}
className="m-b-md"
duration={selectedDays}
label={`${t('label.assigned-entity', {
entity: t('label.tier'),
})} %`}
progress={Number(total)}
showLabel={false}
key={tier}
stroke={TOTAL_ENTITY_CHART_COLOR[i]}
strokeOpacity={
isEmpty(activeMouseHoverKey) || tier === activeMouseHoverKey
? DEFAULT_CHART_OPACITY
: HOVER_CHART_OPACITY
}
type="monotone"
/>
))}
</LineChart>
</ResponsiveContainer>
</Col>
<Col span={DI_STRUCTURE.rightContainerSpan}>
<Row gutter={[8, 16]}>
<Col span={24}>
<DataInsightProgressBar
changeInValue={relativePercentage}
duration={selectedDays}
label={`${t('label.assigned-entity', {
entity: t('label.tier'),
})} %`}
progress={Number(total)}
showLabel={false}
/>
</Col>
<Col span={24}>
<Searchbar
removeMargin
searchValue={searchEntityKeyWord}
onSearch={setSearchEntityKeyWord}
/>
</Col>
<Col className="chart-card-right-panel-container" span={24}>
<Row gutter={[8, 8]}>
{rightSideEntityList.map((tier, i) => {
return (
<Col
className="entity-summary-container"
key={tier}
span={24}
onClick={() => handleLegendClick(tier)}
onMouseEnter={() => handleLegendMouseEnter(tier)}
onMouseLeave={handleLegendMouseLeave}>
<EntitySummaryProgressBar
entity={tier}
isActive={
activeKeys.length ? activeKeys.includes(tier) : true
}
label={round(latestData[tier] || 0, 2) + '%'}
latestData={latestData}
pluralize={false}
progress={latestData[tier]}
strokeColor={TOTAL_ENTITY_CHART_COLOR[i]}
/>
</Col>
);
})}
</Row>
</Col>
{activeKeys.length > 0 && (
<Col className="flex justify-end" span={24}>
<Button type="link" onClick={() => setActiveKeys([])}>
{t('label.clear')}
</Button>
</Col>
{tiers.map((tiers, i) => {
return (
<Col key={tiers} span={24}>
<EntitySummaryProgressBar
entity={tiers}
label={round(latestData[tiers] || 0, 2) + '%'}
latestData={latestData}
pluralize={false}
progress={latestData[tiers]}
strokeColor={TOTAL_ENTITY_CHART_COLOR[i]}
/>
</Col>
);
})}
</Row>
</Col>
</Row>
) : (
<EmptyGraphPlaceholder />
)}
)}
</Row>
</Col>
</Row>
</Card>
);
};

View File

@ -18,8 +18,6 @@ import React, { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
CartesianGrid,
Legend,
LegendProps,
Line,
LineChart,
ResponsiveContainer,
@ -36,6 +34,7 @@ import {
import {
BAR_CHART_MARGIN,
DI_STRUCTURE,
GRAPH_HEIGHT,
TOTAL_ENTITY_CHART_COLOR,
} from '../../constants/DataInsight.constants';
import { DataReportIndex } from '../../generated/dataInsight/dataInsightChart';
@ -45,14 +44,11 @@ import {
} from '../../generated/dataInsight/dataInsightChartResult';
import { ChartFilter } from '../../interface/data-insight.interface';
import { getAggregateChartData } from '../../rest/DataInsightAPI';
import {
axisTickFormatter,
updateActiveChartFilter,
} from '../../utils/ChartUtils';
import { axisTickFormatter } from '../../utils/ChartUtils';
import {
CustomTooltip,
getGraphDataByEntityType,
renderLegend,
sortEntityByValue,
} from '../../utils/DataInsightUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import './DataInsightDetail.less';
@ -81,6 +77,9 @@ const TotalEntityInsight: FC<Props> = ({ chartFilter, selectedDays }) => {
}, [totalEntitiesByType]);
const { t } = useTranslation();
const sortedEntitiesByValue = useMemo(() => {
return sortEntityByValue(entities, latestData);
}, [entities, latestData]);
const fetchTotalEntitiesByType = async () => {
setIsLoading(true);
@ -100,99 +99,92 @@ const TotalEntityInsight: FC<Props> = ({ chartFilter, selectedDays }) => {
}
};
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(() => {
fetchTotalEntitiesByType();
}, [chartFilter]);
if (isLoading || data.length === 0) {
return (
<Card
className="data-insight-card"
loading={isLoading}
title={
<PageHeader
data={{
header: t('label.data-insight-total-entity-summary'),
subHeader: t('message.total-entity-insight'),
}}
/>
}>
<EmptyGraphPlaceholder />
</Card>
);
}
return (
<Card
className="data-insight-card"
data-testid="entity-summary-card"
id={DataInsightChartType.TotalEntitiesByType}
loading={isLoading}
title={
<PageHeader
data={{
header: t('label.data-insight-total-entity-summary'),
subHeader: t('message.total-entity-insight'),
}}
/>
}>
{data.length ? (
<Row gutter={DI_STRUCTURE.rowContainerGutter}>
<Col span={DI_STRUCTURE.leftContainerSpan}>
<ResponsiveContainer
debounce={1}
id="entity-summary-chart"
minHeight={400}>
<LineChart data={data} margin={BAR_CHART_MARGIN}>
<CartesianGrid
stroke={GRAPH_BACKGROUND_COLOR}
vertical={false}
/>
<XAxis dataKey="timestamp" />
<YAxis tickFormatter={(value) => axisTickFormatter(value)} />
<Tooltip content={<CustomTooltip />} />
<Legend
align="left"
content={(props) =>
renderLegend(props as LegendProps, activeKeys)
loading={isLoading}>
<Row gutter={DI_STRUCTURE.rowContainerGutter}>
<Col span={DI_STRUCTURE.leftContainerSpan}>
<PageHeader
data={{
header: t('label.data-insight-total-entity-summary'),
subHeader: t('message.total-entity-insight'),
}}
/>
<ResponsiveContainer
className="m-t-lg"
debounce={1}
height={GRAPH_HEIGHT}
id="entity-summary-chart">
<LineChart data={data} margin={BAR_CHART_MARGIN}>
<CartesianGrid stroke={GRAPH_BACKGROUND_COLOR} vertical={false} />
<XAxis dataKey="timestamp" />
<YAxis tickFormatter={(value) => axisTickFormatter(value)} />
<Tooltip
content={<CustomTooltip />}
wrapperStyle={{ pointerEvents: 'auto' }}
/>
{entities.map((entity, i) => (
<Line
dataKey={entity}
hide={
activeKeys.length && entity !== activeMouseHoverKey
? !activeKeys.includes(entity)
: false
}
layout="horizontal"
verticalAlign="top"
wrapperStyle={{ left: '0px', top: '0px' }}
onClick={handleLegendClick}
onMouseEnter={handleLegendMouseEnter}
onMouseLeave={handleLegendMouseLeave}
key={entity}
stroke={TOTAL_ENTITY_CHART_COLOR[i]}
strokeOpacity={
isEmpty(activeMouseHoverKey) ||
entity === activeMouseHoverKey
? DEFAULT_CHART_OPACITY
: HOVER_CHART_OPACITY
}
type="monotone"
/>
{entities.map((entity, i) => (
<Line
dataKey={entity}
hide={
activeKeys.length && entity !== activeMouseHoverKey
? !activeKeys.includes(entity)
: false
}
key={entity}
stroke={TOTAL_ENTITY_CHART_COLOR[i]}
strokeOpacity={
isEmpty(activeMouseHoverKey) ||
entity === activeMouseHoverKey
? DEFAULT_CHART_OPACITY
: HOVER_CHART_OPACITY
}
type="monotone"
/>
))}
</LineChart>
</ResponsiveContainer>
</Col>
<Col span={DI_STRUCTURE.rightContainerSpan}>
<TotalEntityInsightSummary
entities={entities}
gutter={DI_STRUCTURE.rightRowGutter}
latestData={latestData}
relativePercentage={relativePercentage}
selectedDays={selectedDays}
total={total}
/>
</Col>
</Row>
) : (
<EmptyGraphPlaceholder />
)}
))}
</LineChart>
</ResponsiveContainer>
</Col>
<Col span={DI_STRUCTURE.rightContainerSpan}>
<TotalEntityInsightSummary
allowFilter
activeKeys={activeKeys}
entities={sortedEntitiesByValue}
gutter={DI_STRUCTURE.rightRowGutter}
latestData={latestData}
relativePercentage={relativePercentage}
selectedDays={selectedDays}
total={total}
onActiveKeyMouseHover={(entity) => setActiveMouseHoverKey(entity)}
onActiveKeysUpdate={(entities) => setActiveKeys(entities)}
/>
</Col>
</Row>
</Card>
);
};

View File

@ -10,11 +10,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Col, Row } from 'antd';
import { Button, Col, Row } from 'antd';
import { Gutter } from 'antd/lib/grid/row';
import React from 'react';
import classNames from 'classnames';
import { includes, toLower } from 'lodash';
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { TOTAL_ENTITY_CHART_COLOR } from '../../constants/DataInsight.constants';
import { updateActiveChartFilter } from '../../utils/ChartUtils';
import { sortEntityByValue } from '../../utils/DataInsightUtils';
import Searchbar from '../common/searchbar/Searchbar';
import CustomStatistic from './CustomStatistic';
import EntitySummaryProgressBar from './EntitySummaryProgressBar.component';
@ -25,20 +30,51 @@ type TotalEntityInsightSummaryProps = {
entities: string[];
latestData: Record<string, number>;
gutter?: Gutter | [Gutter, Gutter];
onActiveKeysUpdate?: (entity: string[]) => void;
onActiveKeyMouseHover?: (entity: string) => void;
activeKeys?: string[];
allowFilter?: boolean;
};
const TotalEntityInsightSummary = ({
total,
allowFilter = false,
relativePercentage,
selectedDays,
entities,
latestData,
gutter,
activeKeys,
onActiveKeysUpdate,
onActiveKeyMouseHover,
}: TotalEntityInsightSummaryProps) => {
const { t } = useTranslation();
const [searchEntityKeyWord, setSearchEntityKeyWord] = useState('');
const sortedEntitiesByValue = useMemo(() => {
return sortEntityByValue(entities, latestData);
}, [entities, latestData]);
const rightSideEntityList = useMemo(
() =>
sortedEntitiesByValue.filter((entity) =>
includes(toLower(entity), toLower(searchEntityKeyWord))
),
[sortedEntitiesByValue, searchEntityKeyWord]
);
const handleLegendClick = (entity: string) => {
onActiveKeysUpdate?.(updateActiveChartFilter(entity, activeKeys ?? []));
};
const handleLegendMouseEnter = (entity: string) => {
onActiveKeyMouseHover?.(entity);
};
const handleLegendMouseLeave = () => {
onActiveKeyMouseHover?.('');
};
return (
<Row data-testid="total-entity-insight-summary-container" gutter={gutter}>
<Row data-testid="total-entity-insight-summary-container" gutter={[8, 16]}>
<Col className="p-b-sm" span={24}>
<CustomStatistic
changeInValue={relativePercentage}
@ -49,20 +85,56 @@ const TotalEntityInsightSummary = ({
value={total}
/>
</Col>
{entities.map((entity, i) => {
const progress = (latestData[entity] / Number(total)) * 100;
{allowFilter && (
<Col span={24}>
<Searchbar
removeMargin
searchValue={searchEntityKeyWord}
onSearch={setSearchEntityKeyWord}
/>
</Col>
)}
<Col
className={classNames({
'chart-card-right-panel-container': allowFilter,
})}
span={24}>
<Row gutter={[8, 8]}>
{rightSideEntityList.map((entity, i) => {
const progress = (latestData[entity] / Number(total)) * 100;
return (
<Col key={entity} span={24}>
<EntitySummaryProgressBar
entity={entity}
latestData={latestData}
progress={progress}
strokeColor={TOTAL_ENTITY_CHART_COLOR[i]}
/>
</Col>
);
})}
return (
<Col
className={classNames({
'entity-summary-container': allowFilter,
})}
key={entity}
span={24}
onClick={() => handleLegendClick(entity)}
onMouseEnter={() => handleLegendMouseEnter(entity)}
onMouseLeave={handleLegendMouseLeave}>
<EntitySummaryProgressBar
entity={entity}
isActive={
activeKeys?.length ? activeKeys.includes(entity) : true
}
latestData={latestData}
progress={progress}
strokeColor={TOTAL_ENTITY_CHART_COLOR[i]}
/>
</Col>
);
})}
</Row>
</Col>
{activeKeys && activeKeys.length > 0 && allowFilter && (
<Col className="flex justify-end" span={24}>
<Button type="link" onClick={() => onActiveKeysUpdate?.([])}>
{t('label.clear')}
</Button>
</Col>
)}
</Row>
);
};

View File

@ -34,9 +34,11 @@ export const DI_STRUCTURE = {
rowContainerGutter: 32 as RowProps['gutter'],
leftContainerSpan: 16,
rightContainerSpan: 8,
rightRowGutter: [8, 16] as RowProps['gutter'],
rightRowGutter: [8, 0] as RowProps['gutter'],
};
export const GRAPH_HEIGHT = 500;
export const DATA_INSIGHT_GRAPH_COLORS = [
'#E7B85D',
'#416BB3',

View File

@ -36,7 +36,7 @@ export const TEXT_GREY_MUTED = '#757575';
export const SUCCESS_COLOR = '#008376';
export const DE_ACTIVE_COLOR = '#6B7280';
export const GRAPH_BACKGROUND_COLOR = '#f5f5f5';
export const GRAYED_OUT_COLOR = '#CCCCCC';
export const GRAYED_OUT_COLOR = '#959595';
export const GREEN_COLOR = '#28A745';
export const GREEN_COLOR_OPACITY_30 = '#28A74530';
export const BORDER_COLOR = '#0000001a';

View File

@ -156,3 +156,21 @@
}
}
}
.entity-summary-container {
cursor: pointer;
&:hover {
background: @grey-1;
}
.non-active-details {
.entity-summary-name,
.entity-summary-value {
color: @grey-3;
}
}
}
.chart-card-right-panel-container {
max-height: 375px;
overflow-y: auto;
}

View File

@ -164,28 +164,30 @@ export const CustomTooltip = (props: DataInsightChartTooltipProps) => {
<Card
className="custom-data-insight-tooltip"
title={<Typography.Title level={5}>{timestamp}</Typography.Title>}>
{payload.map((entry, index) => (
<li
className="d-flex items-center justify-between gap-6 p-b-xss text-sm"
key={`item-${index}`}>
<span className="flex items-center text-grey-muted">
<Surface className="mr-2" height={12} version="1.1" width={12}>
<rect fill={entry.color} height="14" rx="2" width="14" />
</Surface>
{startCase(entry.dataKey as string)}
</span>
<span className="font-medium">
{valueFormatter
? valueFormatter(entry.value)
: getEntryFormattedValue(
entry.value,
entry.dataKey,
kpiTooltipRecord,
isPercentage
)}
</span>
</li>
))}
<ul className="custom-data-insight-tooltip-container">
{payload.map((entry, index) => (
<li
className="d-flex items-center justify-between gap-6 p-b-xss text-sm"
key={`item-${index}`}>
<span className="flex items-center text-grey-muted">
<Surface className="mr-2" height={12} version="1.1" width={12}>
<rect fill={entry.color} height="14" rx="2" width="14" />
</Surface>
{startCase(entry.dataKey as string)}
</span>
<span className="font-medium">
{valueFormatter
? valueFormatter(entry.value)
: getEntryFormattedValue(
entry.value,
entry.dataKey,
kpiTooltipRecord,
isPercentage
)}
</span>
</li>
))}
</ul>
</Card>
);
}
@ -679,3 +681,25 @@ export const getOptionalDataInsightTabFlag = (tab: DataInsightTabs) => {
tab === DataInsightTabs.KPIS || tab === DataInsightTabs.DATA_ASSETS,
};
};
export const sortEntityByValue = (
entities: string[],
latestData: Record<string, number>
) => {
const entityValues = entities.map((entity) => ({
entity,
value: latestData[entity] ?? 0,
}));
// Sort the entities based on their values in descending order
entityValues.sort((a, b) => b.value - a.value);
// Extract the sorted entities without their values
return entityValues.map((entity) => entity.entity);
};
export const getRandomHexColor = () => {
const randomColor = Math.floor(Math.random() * 16777215).toString(16);
return `#${randomColor}`;
};