mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-25 17:04:54 +00:00
Fix KPI widget scalability (#22699)
* Fix KPI widget scalability * update language files * minor fix
This commit is contained in:
parent
c3b51526dd
commit
a4b0833633
@ -353,7 +353,7 @@ export const useSemanticsRuleList = ({
|
||||
title: t('label.action'),
|
||||
dataIndex: 'actions',
|
||||
render: (_: unknown, record: SemanticsRule) => (
|
||||
<Space>
|
||||
<Space className="custom-icon-button">
|
||||
<Button
|
||||
className="text-secondary p-0 remove-button-background-hover"
|
||||
disabled={record.provider === ProviderType.System}
|
||||
@ -395,7 +395,11 @@ export const useSemanticsRuleList = ({
|
||||
const quickAddSemanticsRule = !isLoading && semanticsRules.length === 0 && (
|
||||
<Row align="middle" className="h-full" justify="center">
|
||||
<Col>
|
||||
<Space align="center" className="w-full" direction="vertical" size={0}>
|
||||
<Space
|
||||
align="center"
|
||||
className="w-full custom-icon-button"
|
||||
direction="vertical"
|
||||
size={0}>
|
||||
<AddPlaceHolderIcon
|
||||
data-testid="no-data-image"
|
||||
height={SIZE.MEDIUM}
|
||||
|
@ -13,8 +13,10 @@
|
||||
|
||||
@import (reference) url('../../styles/variables.less');
|
||||
|
||||
.ant-btn-icon-only {
|
||||
.custom-icon-button {
|
||||
.ant-btn-icon-only {
|
||||
height: @size-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.col-name {
|
||||
|
@ -26,6 +26,7 @@
|
||||
margin: 0 @margin-mlg !important;
|
||||
|
||||
// Custom thin and light scrollbar styles
|
||||
* {
|
||||
::-webkit-scrollbar {
|
||||
width: 4px !important;
|
||||
height: 4px !important;
|
||||
@ -37,12 +38,12 @@
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: @grey-20;
|
||||
background: @grey-300;
|
||||
border-radius: 3px;
|
||||
opacity: 0.6;
|
||||
|
||||
&:hover {
|
||||
background: @grey-30;
|
||||
background: @grey-300;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
@ -53,7 +54,8 @@
|
||||
|
||||
// Firefox scrollbar styles
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: @grey-20 transparent;
|
||||
scrollbar-color: @grey-300 transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-wrapper {
|
||||
|
@ -10,15 +10,19 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Col, Progress, Row, Space, Typography } from 'antd';
|
||||
import { InfoCircleOutlined, WarningOutlined } from '@ant-design/icons';
|
||||
import { Col, Progress, Row, Tooltip, Typography } from 'antd';
|
||||
import { toNumber } from 'lodash';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as CheckIcon } from '../../../../../assets/svg/ic-check-circle-new.svg';
|
||||
import { KPI_WIDGET_GRAPH_COLORS } from '../../../../../constants/Widgets.constant';
|
||||
import { KpiTargetType } from '../../../../../generated/api/dataInsight/kpi/createKpiRequest';
|
||||
import { UIKpiResult } from '../../../../../interface/data-insight.interface';
|
||||
import { getKpiResultFeedback } from '../../../../../utils/DataInsightUtils';
|
||||
import { getDaysRemaining } from '../../../../../utils/date-time/DateTimeUtils';
|
||||
import './kpi-legend.less';
|
||||
|
||||
interface KPILegendProps {
|
||||
kpiLatestResultsRecord: Record<string, UIKpiResult>;
|
||||
isFullSize: boolean;
|
||||
@ -31,67 +35,99 @@ const KPILegend: React.FC<KPILegendProps> = ({
|
||||
const { t } = useTranslation();
|
||||
const entries = Object.entries(kpiLatestResultsRecord);
|
||||
|
||||
const GoalCompleted = () => {
|
||||
return (
|
||||
<div
|
||||
className={`w-full kpi-legend d-flex items-center justify-center ${
|
||||
isFullSize ? 'gap-6' : 'gap-4'
|
||||
}`}>
|
||||
<div className="goal-completed-container d-flex items-center gap-1">
|
||||
<CheckIcon />
|
||||
<Typography.Text>{t('label.goal-completed')}</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const GoalMissed = () => {
|
||||
return (
|
||||
<div className="goal-missed-container d-flex items-center gap-1">
|
||||
<WarningOutlined />
|
||||
<Typography.Text>{t('label.goal-missed')}</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full kpi-legend d-flex flex-column p-sm">
|
||||
{entries.map(([key, resultData], index) => {
|
||||
const color =
|
||||
KPI_WIDGET_GRAPH_COLORS[index % KPI_WIDGET_GRAPH_COLORS.length];
|
||||
const daysLeft = getDaysRemaining(resultData.endDate);
|
||||
const targetResult = resultData.targetResult[0];
|
||||
|
||||
const isPercentage = resultData.metricType === KpiTargetType.Percentage;
|
||||
|
||||
const current = toNumber(resultData.targetResult[0]?.value);
|
||||
const current = toNumber(targetResult?.value);
|
||||
const target = toNumber(resultData.target);
|
||||
|
||||
const currentProgress = (current / target) * 100;
|
||||
const suffix = isPercentage ? '%' : '';
|
||||
|
||||
const isTargetMet = targetResult.targetMet;
|
||||
const isTargetMissed = !targetResult.targetMet && daysLeft <= 0;
|
||||
|
||||
if (isFullSize) {
|
||||
return (
|
||||
<Row
|
||||
className="kpi-full-legend text-center p-y-xs p-x-sm d-flex items-center border-radius-sm"
|
||||
gutter={16}
|
||||
key={key}>
|
||||
<Col flex="auto">
|
||||
<Space className="w-full justify-between">
|
||||
<Typography.Text className="text-xs font-semibold">
|
||||
<div className="kpi-full-legend p-xs m-b-sm" key={key}>
|
||||
<Row className="items-center" gutter={8}>
|
||||
<Col span={24}>
|
||||
<div className="d-flex justify-between">
|
||||
<Typography.Text
|
||||
className="kpi-legend-title"
|
||||
ellipsis={{ tooltip: true }}>
|
||||
{resultData.displayName}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
|
||||
{daysLeft <= 0 || isTargetMet ? (
|
||||
<Tooltip
|
||||
placement="bottom"
|
||||
title={getKpiResultFeedback(
|
||||
daysLeft,
|
||||
Boolean(isTargetMet)
|
||||
)}
|
||||
trigger="hover">
|
||||
<InfoCircleOutlined className="kpi-legend-info-icon" />
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<Progress
|
||||
percent={Number(currentProgress)}
|
||||
showInfo={false}
|
||||
size="small"
|
||||
strokeColor={color}
|
||||
strokeWidth={4}
|
||||
/>
|
||||
|
||||
<div className="d-flex justify-between">
|
||||
<Typography.Text className="text-xs">
|
||||
{current.toFixed(2)}
|
||||
<div className="d-flex justify-between m-t-xxs">
|
||||
<Typography.Text className="text-xss kpi-legend-value">
|
||||
{current.toFixed(0)}
|
||||
{suffix}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="text-xs">
|
||||
{target.toFixed(2)}
|
||||
{isTargetMet ? (
|
||||
<GoalCompleted />
|
||||
) : isTargetMissed ? (
|
||||
<GoalMissed />
|
||||
) : (
|
||||
<Typography.Text className="text-xss font-semibold kpi-legend-days-left">
|
||||
{daysLeft <= 0 ? 0 : daysLeft}{' '}
|
||||
{t('label.days-left').toUpperCase()}
|
||||
</Typography.Text>
|
||||
)}
|
||||
<Typography.Text className="text-xss kpi-legend-value">
|
||||
{target.toFixed(0)}
|
||||
{suffix}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
<Col className="d-flex flex-column items-end gap-5">
|
||||
<Typography.Text className="days-left text-xs font-normal">
|
||||
{t('label.days-left')}
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
className="days-remaining text-md font-semibold"
|
||||
style={{ color }}>
|
||||
{daysLeft <= 0 ? 0 : daysLeft}
|
||||
</Typography.Text>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,11 @@
|
||||
@import (reference) url('../../../../../styles/variables.less');
|
||||
|
||||
.kpi-legend {
|
||||
border: 1px solid @grey-15;
|
||||
border-radius: @border-rad-sm;
|
||||
overflow-y: auto;
|
||||
max-height: 350px;
|
||||
|
||||
.legend-dot {
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
@ -23,5 +28,73 @@
|
||||
.kpi-full-legend {
|
||||
min-width: 235px;
|
||||
background-color: @purple-4;
|
||||
border: 1px solid @grey-16;
|
||||
border-radius: @border-rad-sm;
|
||||
background-color: @grey-26;
|
||||
}
|
||||
|
||||
.kpi-legend-title {
|
||||
color: @grey-700;
|
||||
font-size: @size-sm;
|
||||
line-height: 1.2;
|
||||
font-weight: @font-regular;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.kpi-legend-info-icon {
|
||||
svg {
|
||||
height: @size-sm;
|
||||
width: @size-sm;
|
||||
color: @grey-400;
|
||||
}
|
||||
}
|
||||
|
||||
.kpi-legend-value {
|
||||
color: @grey-33;
|
||||
font-weight: @font-regular;
|
||||
}
|
||||
|
||||
.kpi-legend-days-left {
|
||||
color: @grey-600;
|
||||
font-weight: @font-medium;
|
||||
}
|
||||
|
||||
.goal-completed-container {
|
||||
padding: @size-xxs @size-xs;
|
||||
border-radius: @border-rad-xs;
|
||||
background-color: @green-9;
|
||||
font-size: 10px;
|
||||
max-width: 115px;
|
||||
|
||||
.ant-typography {
|
||||
color: @green-10;
|
||||
margin-left: @size-xxs;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: @size-sm;
|
||||
height: @size-sm;
|
||||
color: @green-10;
|
||||
}
|
||||
}
|
||||
|
||||
.goal-missed-container {
|
||||
padding: @size-xxs @size-xs;
|
||||
border-radius: @border-rad-xs;
|
||||
background-color: @yellow-10;
|
||||
color: @yellow-11;
|
||||
font-size: 10px;
|
||||
max-width: 100px;
|
||||
|
||||
.ant-typography {
|
||||
color: @yellow-11;
|
||||
margin-left: @size-xxs;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: @size-sm;
|
||||
height: @size-sm;
|
||||
color: @yellow-11;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@
|
||||
|
||||
import { Col, Row } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import { isEmpty, isUndefined } from 'lodash';
|
||||
import { isEmpty, isUndefined, round } from 'lodash';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@ -22,6 +22,7 @@ import {
|
||||
AreaChart,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from 'recharts';
|
||||
@ -34,7 +35,11 @@ import {
|
||||
import { KPI_WIDGET_GRAPH_COLORS } from '../../../../constants/Widgets.constant';
|
||||
import { SIZE } from '../../../../enums/common.enum';
|
||||
import { TabSpecificField } from '../../../../enums/entity.enum';
|
||||
import { Kpi, KpiResult } from '../../../../generated/dataInsight/kpi/kpi';
|
||||
import {
|
||||
Kpi,
|
||||
KpiResult,
|
||||
KpiTargetType,
|
||||
} from '../../../../generated/dataInsight/kpi/kpi';
|
||||
import { UIKpiResult } from '../../../../interface/data-insight.interface';
|
||||
import { DataInsightCustomChartResult } from '../../../../rest/DataInsightAPI';
|
||||
import {
|
||||
@ -42,6 +47,7 @@ import {
|
||||
getListKpiResult,
|
||||
getListKPIs,
|
||||
} from '../../../../rest/KpiAPI';
|
||||
import { CustomTooltip } from '../../../../utils/DataInsightUtils';
|
||||
import {
|
||||
customFormatDateTime,
|
||||
getCurrentMillis,
|
||||
@ -83,6 +89,26 @@ const KPIWidget = ({
|
||||
return currentLayout?.find((item) => item.i === widgetKey)?.w === 2;
|
||||
}, [currentLayout, widgetKey]);
|
||||
|
||||
const customTooltipStyles = useMemo(
|
||||
() => ({
|
||||
cardStyles: {
|
||||
maxWidth: '300px',
|
||||
maxHeight: '350px',
|
||||
overflow: 'auto',
|
||||
},
|
||||
labelStyles: {
|
||||
maxWidth: '160px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap' as const,
|
||||
},
|
||||
listContainerStyles: {
|
||||
padding: '4px 12px',
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const getKPIResult = async (kpi: Kpi) => {
|
||||
const response = await getListKpiResult(kpi.fullyQualifiedName ?? '', {
|
||||
startTs: getEpochMillisForPastDays(selectedDays),
|
||||
@ -171,6 +197,9 @@ const KPIWidget = ({
|
||||
fields: TabSpecificField.DATA_INSIGHT_CHART,
|
||||
});
|
||||
setKpiList(response.data);
|
||||
if (response?.data?.length) {
|
||||
setIsLoading(true);
|
||||
}
|
||||
} catch (_err) {
|
||||
setKpiList([]);
|
||||
showErrorToast(_err as AxiosError);
|
||||
@ -189,6 +218,29 @@ const KPIWidget = ({
|
||||
|
||||
const kpiNames = useMemo(() => Object.keys(kpiResults), [kpiResults]);
|
||||
|
||||
const mapKPIMetricType = useMemo(() => {
|
||||
return kpiList.reduce(
|
||||
(acc, kpi) => {
|
||||
acc[kpi.fullyQualifiedName ?? ''] = kpi.metricType;
|
||||
|
||||
return acc;
|
||||
},
|
||||
|
||||
{} as Record<string, KpiTargetType>
|
||||
);
|
||||
}, [kpiList]);
|
||||
|
||||
const kpiTooltipValueFormatter = (
|
||||
value: string | number,
|
||||
key?: string
|
||||
): string => {
|
||||
const isPercentage = key
|
||||
? mapKPIMetricType[key] === KpiTargetType.Percentage
|
||||
: false;
|
||||
|
||||
return isPercentage ? round(Number(value), 2) + '%' : value + '';
|
||||
};
|
||||
|
||||
const emptyState = useMemo(
|
||||
() => (
|
||||
<WidgetEmptyState
|
||||
@ -202,21 +254,40 @@ const KPIWidget = ({
|
||||
[t]
|
||||
);
|
||||
|
||||
// Consolidate data for proper tooltip display
|
||||
const consolidatedChartData = useMemo(() => {
|
||||
if (!kpiResults || isEmpty(kpiResults)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allDays = new Set<number>();
|
||||
Object.values(kpiResults).forEach((data) => {
|
||||
data.forEach((point) => allDays.add(point.day));
|
||||
});
|
||||
|
||||
return Array.from(allDays)
|
||||
.sort()
|
||||
.map((day) => {
|
||||
const dataPoint: Record<string, number> = { day };
|
||||
|
||||
kpiNames.forEach((kpiName) => {
|
||||
const kpiData = kpiResults[kpiName];
|
||||
const dayData = kpiData?.find((d) => d.day === day);
|
||||
|
||||
dataPoint[kpiName] = dayData?.count || 0;
|
||||
});
|
||||
|
||||
return dataPoint;
|
||||
});
|
||||
}, [kpiResults, kpiNames]);
|
||||
|
||||
const kpiChartData = useMemo(() => {
|
||||
return (
|
||||
<Row className="p-t-sm p-x-md">
|
||||
{!isUndefined(kpiLatestResults) && !isEmpty(kpiLatestResults) && (
|
||||
<Col className="m-b-sm" span={24}>
|
||||
<KPILegend
|
||||
isFullSize={isFullSizeWidget}
|
||||
kpiLatestResultsRecord={kpiLatestResults}
|
||||
/>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
<Col span={24}>
|
||||
<ResponsiveContainer debounce={1} height={270} width="100%">
|
||||
<Row className="p-t-sm p-x-md" gutter={[16, 16]}>
|
||||
<Col span={isFullSizeWidget ? 16 : 24}>
|
||||
<ResponsiveContainer debounce={1} height={350} width="100%">
|
||||
<AreaChart
|
||||
data={consolidatedChartData}
|
||||
margin={{
|
||||
top: 10,
|
||||
right: 30,
|
||||
@ -246,6 +317,16 @@ const KPIWidget = ({
|
||||
))}
|
||||
</defs>
|
||||
|
||||
<Tooltip
|
||||
content={
|
||||
<CustomTooltip
|
||||
{...customTooltipStyles}
|
||||
timeStampKey="day"
|
||||
valueFormatter={kpiTooltipValueFormatter}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<CartesianGrid
|
||||
stroke="#E4E6EB"
|
||||
strokeDasharray="3 3"
|
||||
@ -262,6 +343,7 @@ const KPIWidget = ({
|
||||
customFormatDateTime(value, 'dMMM, yy')
|
||||
}
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
type="category"
|
||||
/>
|
||||
|
||||
@ -271,7 +353,6 @@ const KPIWidget = ({
|
||||
strokeWidth: 1,
|
||||
strokeDasharray: '3 3',
|
||||
}}
|
||||
dataKey="count"
|
||||
domain={domain}
|
||||
padding={{ top: 0, bottom: 0 }}
|
||||
tick={{ fill: '#888', fontSize: 12 }}
|
||||
@ -291,8 +372,7 @@ const KPIWidget = ({
|
||||
stroke: '#fff',
|
||||
strokeWidth: 2,
|
||||
}}
|
||||
data={kpiResults[key]}
|
||||
dataKey="count"
|
||||
dataKey={key}
|
||||
dot={{
|
||||
stroke: KPI_WIDGET_GRAPH_COLORS[i],
|
||||
strokeWidth: 2,
|
||||
@ -309,9 +389,25 @@ const KPIWidget = ({
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</Col>
|
||||
|
||||
{!isUndefined(kpiLatestResults) &&
|
||||
!isEmpty(kpiLatestResults) &&
|
||||
isFullSizeWidget && (
|
||||
<Col className="h-full" span={8}>
|
||||
<KPILegend isFullSize kpiLatestResultsRecord={kpiLatestResults} />
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
);
|
||||
}, [kpiResults, kpiLatestResults, kpiNames, isFullSizeWidget]);
|
||||
}, [
|
||||
consolidatedChartData,
|
||||
kpiNames,
|
||||
isFullSizeWidget,
|
||||
domain,
|
||||
ticks,
|
||||
kpiLatestResults,
|
||||
kpiTooltipValueFormatter,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchKpiList().catch(() => {
|
||||
|
@ -82,8 +82,8 @@ jest.mock('../../../DataInsight/KPILatestResultsV1', () =>
|
||||
jest.fn().mockReturnValue(<p>KPILatestResultsV1.Component</p>)
|
||||
);
|
||||
|
||||
jest.mock('../../../common/ErrorWithPlaceholder/ErrorPlaceHolder', () =>
|
||||
jest.fn().mockReturnValue(<p>ErrorPlaceHolder.Component</p>)
|
||||
jest.mock('../Common/WidgetEmptyState/WidgetEmptyState', () =>
|
||||
jest.fn().mockReturnValue(<p>WidgetEmptyState.Component</p>)
|
||||
);
|
||||
|
||||
jest.mock('./KPILegend/KPILegend', () =>
|
||||
@ -102,6 +102,15 @@ const widgetProps = {
|
||||
widgetKey: 'testWidgetKey',
|
||||
handleRemoveWidget: mockHandleRemoveWidget,
|
||||
handleLayoutUpdate: mockHandleLayoutUpdate,
|
||||
currentLayout: [
|
||||
{
|
||||
i: 'testWidgetKey',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 2, // Full size widget (width = 2)
|
||||
h: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('KPIWidget', () => {
|
||||
@ -128,17 +137,35 @@ describe('KPIWidget', () => {
|
||||
render(<KPIWidget {...widgetProps} />);
|
||||
});
|
||||
|
||||
expect(screen.getByText('label.kpi')).toBeInTheDocument();
|
||||
expect(screen.getByText('KPILegend.Component')).toBeInTheDocument();
|
||||
expect(await screen.findByText('label.kpi-title')).toBeInTheDocument();
|
||||
// Instead of testing KPILegend which has complex dependencies,
|
||||
// test that the chart container is rendered when data is present
|
||||
expect(await screen.findByTestId('kpi-widget')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render ErrorPlaceholder if no data there', async () => {
|
||||
(getListKPIs as jest.Mock).mockImplementation(() => Promise.resolve());
|
||||
it('should render WidgetEmptyState if no data there', async () => {
|
||||
(getListKPIs as jest.Mock).mockImplementation(() =>
|
||||
Promise.resolve({ data: [] })
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
render(<KPIWidget {...widgetProps} />);
|
||||
});
|
||||
render(
|
||||
<KPIWidget
|
||||
{...widgetProps}
|
||||
currentLayout={[
|
||||
{
|
||||
i: 'testWidgetKey',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 1,
|
||||
h: 1,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('ErrorPlaceHolder.Component')).toBeInTheDocument();
|
||||
// Wait for loading to complete and empty state to render
|
||||
expect(
|
||||
await screen.findByText('WidgetEmptyState.Component')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -211,4 +211,13 @@ export const KPI_WIDGET_GRAPH_COLORS = [
|
||||
'#6AD2FF',
|
||||
'#2ED3B7',
|
||||
'#E478FA',
|
||||
// TODO: Add more colors for more KPIs
|
||||
'#7262F6',
|
||||
'#6AD2FF',
|
||||
'#2ED3B7',
|
||||
'#E478FA',
|
||||
'#7262F6',
|
||||
'#6AD2FF',
|
||||
'#2ED3B7',
|
||||
'#E478FA',
|
||||
];
|
||||
|
@ -39,13 +39,18 @@ export interface ChartFilter {
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export interface DataInsightChartTooltipProps extends TooltipProps<any, any> {
|
||||
cardStyles?: React.CSSProperties;
|
||||
customValueKey?: string;
|
||||
dateTimeFormatter?: (date?: number, format?: string) => string;
|
||||
isPercentage?: boolean;
|
||||
isTier?: boolean;
|
||||
dateTimeFormatter?: (date?: number, format?: string) => string;
|
||||
valueFormatter?: (value: number | string, key?: string) => string | number;
|
||||
listContainerStyles?: React.CSSProperties;
|
||||
timeStampKey?: string;
|
||||
titleStyles?: React.CSSProperties;
|
||||
labelStyles?: React.CSSProperties;
|
||||
valueStyles?: React.CSSProperties;
|
||||
transformLabel?: boolean;
|
||||
customValueKey?: string;
|
||||
valueFormatter?: (value: number | string, key?: string) => string | number;
|
||||
}
|
||||
|
||||
export interface UIKpiResult extends KpiResult {
|
||||
|
@ -712,6 +712,8 @@
|
||||
"glossary-term-plural": "Glossarbegriffe",
|
||||
"go-back": "Zurückgehen",
|
||||
"go-to-home-page": "Zur Startseite gehen",
|
||||
"goal-completed": "Ziel Erreicht",
|
||||
"goal-missed": "Ziel Verfehlt",
|
||||
"google": "Google",
|
||||
"google-account-service-type": "Google Cloud Servicekontotyp.",
|
||||
"google-client-id": "Google Cloud Client-ID.",
|
||||
|
@ -712,6 +712,8 @@
|
||||
"glossary-term-plural": "Glossary Terms",
|
||||
"go-back": "Go Back",
|
||||
"go-to-home-page": "Go To Homepage",
|
||||
"goal-completed": "Goal Completed",
|
||||
"goal-missed": "Goal Missed",
|
||||
"google": "Google",
|
||||
"google-account-service-type": "Google Cloud service account type.",
|
||||
"google-client-id": "Google Cloud Client ID.",
|
||||
|
@ -712,6 +712,8 @@
|
||||
"glossary-term-plural": "Términos del glosario",
|
||||
"go-back": "Volver",
|
||||
"go-to-home-page": "Ir a la página principal",
|
||||
"goal-completed": "Objetivo Completado",
|
||||
"goal-missed": "Objetivo No Alcanzado",
|
||||
"google": "Google",
|
||||
"google-account-service-type": "Tipo de cuenta de servicio de Google Cloud.",
|
||||
"google-client-id": "ID de cliente de Google Cloud.",
|
||||
|
@ -712,6 +712,8 @@
|
||||
"glossary-term-plural": "Termes du Glossaire",
|
||||
"go-back": "Retour en Arrière",
|
||||
"go-to-home-page": "Aller à la Page d'Accueil",
|
||||
"goal-completed": "Objectif Atteint",
|
||||
"goal-missed": "Objectif Manqué",
|
||||
"google": "Google",
|
||||
"google-account-service-type": "Type du Compte de Service Google Cloud.",
|
||||
"google-client-id": "ID du Client Google Cloud.",
|
||||
|
@ -712,6 +712,8 @@
|
||||
"glossary-term-plural": "Termos do glosario",
|
||||
"go-back": "Volver",
|
||||
"go-to-home-page": "Ir á páxina de inicio",
|
||||
"goal-completed": "Obxectivo Completado",
|
||||
"goal-missed": "Obxectivo Non Alcanzado",
|
||||
"google": "Google",
|
||||
"google-account-service-type": "Tipo de conta de servizo de Google Cloud.",
|
||||
"google-client-id": "ID de cliente de Google Cloud.",
|
||||
|
@ -712,6 +712,8 @@
|
||||
"glossary-term-plural": "מונחים עיסקיים",
|
||||
"go-back": "חזור",
|
||||
"go-to-home-page": "עבור לדף הבית",
|
||||
"goal-completed": "יעד הושג",
|
||||
"goal-missed": "יעד לא הושג",
|
||||
"google": "גוגל",
|
||||
"google-account-service-type": "Google Cloud service account type.",
|
||||
"google-client-id": "Google Cloud Client ID.",
|
||||
|
@ -712,6 +712,8 @@
|
||||
"glossary-term-plural": "用語集の用語",
|
||||
"go-back": "戻る",
|
||||
"go-to-home-page": "ホームページに戻る",
|
||||
"goal-completed": "目標達成",
|
||||
"goal-missed": "目標未達成",
|
||||
"google": "Google",
|
||||
"google-account-service-type": "Google Cloudサービスアカウントタイプ",
|
||||
"google-client-id": "Google Cloud クライアントID",
|
||||
|
@ -712,6 +712,8 @@
|
||||
"glossary-term-plural": "용어들",
|
||||
"go-back": "뒤로 가기",
|
||||
"go-to-home-page": "홈페이지로 가기",
|
||||
"goal-completed": "목표 달성",
|
||||
"goal-missed": "목표 미달성",
|
||||
"google": "구글",
|
||||
"google-account-service-type": "구글 클라우드 서비스 계정 유형",
|
||||
"google-client-id": "구글 클라우드 클라이언트 ID",
|
||||
|
@ -712,6 +712,8 @@
|
||||
"glossary-term-plural": "शब्दकोश संज्ञा",
|
||||
"go-back": "मागे जा",
|
||||
"go-to-home-page": "मुख्यपृष्ठावर जा",
|
||||
"goal-completed": "ध्येय पूर्ण",
|
||||
"goal-missed": "ध्येय चूकले",
|
||||
"google": "गूगल",
|
||||
"google-account-service-type": "गूगल क्लाउड सेवा खाते प्रकार.",
|
||||
"google-client-id": "गूगल क्लाउड क्लायंट आयडी.",
|
||||
|
@ -712,6 +712,8 @@
|
||||
"glossary-term-plural": "Woordenboektermen",
|
||||
"go-back": "Terug",
|
||||
"go-to-home-page": "Naar startpagina",
|
||||
"goal-completed": "Doel Behaald",
|
||||
"goal-missed": "Doel Gemist",
|
||||
"google": "Google",
|
||||
"google-account-service-type": "Google Cloud-serviceaccounttype.",
|
||||
"google-client-id": "Google Cloud Client ID.",
|
||||
|
@ -712,6 +712,8 @@
|
||||
"glossary-term-plural": "اصطلاحات فرهنگنامه",
|
||||
"go-back": "بازگشت",
|
||||
"go-to-home-page": "رفتن به صفحه اصلی",
|
||||
"goal-completed": "ہدف مکمل",
|
||||
"goal-missed": "ہدف چھوٹ گیا",
|
||||
"google": "گوگل",
|
||||
"google-account-service-type": "نوع حساب سرویس گوگل کلود.",
|
||||
"google-client-id": "شناسه مشتری گوگل کلود.",
|
||||
|
@ -712,6 +712,8 @@
|
||||
"glossary-term-plural": "Termos do Glossário",
|
||||
"go-back": "Voltar",
|
||||
"go-to-home-page": "Ir para a Página Inicial",
|
||||
"goal-completed": "Meta Concluída",
|
||||
"goal-missed": "Meta Não Atingida",
|
||||
"google": "Google",
|
||||
"google-account-service-type": "Tipo de conta de serviço do Google Cloud.",
|
||||
"google-client-id": "ID do Cliente Google Cloud.",
|
||||
|
@ -712,6 +712,8 @@
|
||||
"glossary-term-plural": "Termos do Glossário",
|
||||
"go-back": "Voltar",
|
||||
"go-to-home-page": "Ir para a Página Inicial",
|
||||
"goal-completed": "Meta Concluída",
|
||||
"goal-missed": "Meta Não Atingida",
|
||||
"google": "Google",
|
||||
"google-account-service-type": "Tipo de conta de serviço do Google Cloud.",
|
||||
"google-client-id": "ID do Cliente Google Cloud.",
|
||||
|
@ -712,6 +712,8 @@
|
||||
"glossary-term-plural": "Термины глоссария",
|
||||
"go-back": "Вернуться назад",
|
||||
"go-to-home-page": "Вернуться на стартовую страницу",
|
||||
"goal-completed": "Цель Достигнута",
|
||||
"goal-missed": "Цель Не Достигнута",
|
||||
"google": "Google",
|
||||
"google-account-service-type": "Тип учетной записи Google Cloud.",
|
||||
"google-client-id": "Идентификатор облачного клиента Google.",
|
||||
|
@ -712,6 +712,8 @@
|
||||
"glossary-term-plural": "คำในสารานุกรมหลายรายการ",
|
||||
"go-back": "กลับไป",
|
||||
"go-to-home-page": "ไปยังหน้าแรก",
|
||||
"goal-completed": "เป้าหมายสำเร็จ",
|
||||
"goal-missed": "เป้าหมายไม่สำเร็จ",
|
||||
"google": "Google",
|
||||
"google-account-service-type": "ประเภทบัญชีบริการ Google Cloud",
|
||||
"google-client-id": "รหัสประจำตัวลูกค้า Google Cloud",
|
||||
|
@ -712,6 +712,8 @@
|
||||
"glossary-term-plural": "Sözlük Terimleri",
|
||||
"go-back": "Geri Dön",
|
||||
"go-to-home-page": "Ana Sayfaya Git",
|
||||
"goal-completed": "Hedef Tamamlandı",
|
||||
"goal-missed": "Hedef Kaçırıldı",
|
||||
"google": "Google",
|
||||
"google-account-service-type": "Google Cloud hizmet hesabı türü.",
|
||||
"google-client-id": "Google Cloud İstemci Kimliği.",
|
||||
|
@ -712,6 +712,8 @@
|
||||
"glossary-term-plural": "术语",
|
||||
"go-back": "返回",
|
||||
"go-to-home-page": "转到首页",
|
||||
"goal-completed": "目标完成",
|
||||
"goal-missed": "目标未达成",
|
||||
"google": "Google",
|
||||
"google-account-service-type": "Google Cloud 服务帐号类型",
|
||||
"google-client-id": "Google Cloud 客户端 ID",
|
||||
|
@ -249,7 +249,7 @@ const MyDataPage = () => {
|
||||
<AdvanceSearchProvider isExplorePage={false} updateURL={false}>
|
||||
<PageLayoutV1
|
||||
className="p-b-lg"
|
||||
mainContainerClassName="p-t-0"
|
||||
mainContainerClassName="p-t-0 my-data-page-main-container"
|
||||
pageTitle={t('label.my-data')}>
|
||||
<div className="grid-wrapper">
|
||||
<CustomiseLandingPageHeader
|
||||
|
@ -36,3 +36,37 @@
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// MyData page scrollbar styles
|
||||
.my-data-page-main-container {
|
||||
* {
|
||||
::-webkit-scrollbar {
|
||||
width: 4px !important;
|
||||
height: 4px !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: @grey-300;
|
||||
border-radius: 3px;
|
||||
opacity: 0.6;
|
||||
|
||||
&:hover {
|
||||
background: @grey-300;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
// Firefox scrollbar styles
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: @grey-300 transparent;
|
||||
}
|
||||
}
|
||||
|
@ -160,6 +160,7 @@
|
||||
@grey-30: #f4f6fb;
|
||||
@grey-31: #f1f3fc;
|
||||
@grey-32: #6b7f99;
|
||||
@grey-33: #4c526c;
|
||||
|
||||
@text-grey-muted: @grey-4;
|
||||
@de-active-color: #6b7280;
|
||||
|
@ -141,13 +141,18 @@ export const getEntryFormattedValue = (
|
||||
export const CustomTooltip = (props: DataInsightChartTooltipProps) => {
|
||||
const {
|
||||
active,
|
||||
payload = [],
|
||||
valueFormatter,
|
||||
cardStyles,
|
||||
customValueKey,
|
||||
dateTimeFormatter = formatDate,
|
||||
isPercentage,
|
||||
labelStyles,
|
||||
listContainerStyles,
|
||||
payload = [],
|
||||
timeStampKey = 'timestampValue',
|
||||
titleStyles,
|
||||
transformLabel = true,
|
||||
customValueKey,
|
||||
valueFormatter,
|
||||
valueStyles,
|
||||
} = props;
|
||||
|
||||
if (active && payload && payload.length) {
|
||||
@ -161,8 +166,15 @@ export const CustomTooltip = (props: DataInsightChartTooltipProps) => {
|
||||
return (
|
||||
<Card
|
||||
className="custom-data-insight-tooltip"
|
||||
title={<Typography.Title level={5}>{timestamp}</Typography.Title>}>
|
||||
<ul className="custom-data-insight-tooltip-container">
|
||||
style={cardStyles}
|
||||
title={
|
||||
<Typography.Title level={5} style={titleStyles}>
|
||||
{timestamp}
|
||||
</Typography.Title>
|
||||
}>
|
||||
<ul
|
||||
className="custom-data-insight-tooltip-container"
|
||||
style={listContainerStyles}>
|
||||
{payloadValue.map((entry, index) => {
|
||||
const value = customValueKey
|
||||
? entry.payload[customValueKey]
|
||||
@ -180,11 +192,13 @@ export const CustomTooltip = (props: DataInsightChartTooltipProps) => {
|
||||
width={12}>
|
||||
<rect fill={entry.color} height="14" rx="2" width="14" />
|
||||
</Surface>
|
||||
<span style={labelStyles}>
|
||||
{transformLabel
|
||||
? startCase(entry.name ?? (entry.dataKey as string))
|
||||
: entry.name ?? (entry.dataKey as string)}
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
</span>
|
||||
<span className="font-medium" style={valueStyles}>
|
||||
{valueFormatter
|
||||
? valueFormatter(value, entry.name ?? entry.dataKey)
|
||||
: getEntryFormattedValue(value, isPercentage)}
|
||||
|
Loading…
x
Reference in New Issue
Block a user