This is an automated email from the ASF dual-hosted git repository.
pawarprasad123 pushed a commit to branch atlas-2.6
in repository https://gitbox.apache.org/repos/asf/atlas.git
The following commit(s) were added to refs/heads/atlas-2.6 by this push:
new 218084d3a ATLAS-5329: Atlas React UI: Long classification names
overflow and break the layout in the Classification Distribution (#681) (
cherry-icked from the commit 59081c4eaa9bf1b6c56daab79f5bd2241a7ea24b)
218084d3a is described below
commit 218084d3afd597c24abfddaed0d958de072cb001
Author: Brijesh Bhalala <[email protected]>
AuthorDate: Tue Jul 7 11:53:33 2026 +0530
ATLAS-5329: Atlas React UI: Long classification names overflow and break
the layout in the Classification Distribution (#681)
( cherry-icked from the commit 59081c4eaa9bf1b6c56daab79f5bd2241a7ea24b)
---
.../ClassificationDistributionCard.tsx | 23 +++--
.../ClassificationDistributionCard.test.tsx | 113 +++++++++++++++++++++
.../__tests__/dashboardChartPalette.test.ts | 112 ++++++++++++++++++++
.../DashboardOverview/dashboardChartPalette.ts | 38 ++++++-
4 files changed, 275 insertions(+), 11 deletions(-)
diff --git
a/dashboard/src/views/DashboardOverview/ClassificationDistributionCard.tsx
b/dashboard/src/views/DashboardOverview/ClassificationDistributionCard.tsx
index 6da4704c7..41b2741be 100644
--- a/dashboard/src/views/DashboardOverview/ClassificationDistributionCard.tsx
+++ b/dashboard/src/views/DashboardOverview/ClassificationDistributionCard.tsx
@@ -38,6 +38,9 @@ import { navigateToSearch, navigateToClassificationSearch }
from "@utils/dashboa
import {
CHART_BAR_ACTIVE_BLUE,
CLASSIFICATION_DISTRIBUTION_CHART_MARGIN,
+ getClassificationYAxisWidth,
+ isClassificationYAxisLabelTruncated,
+ truncateClassificationYAxisLabel,
} from "./dashboardChartPalette";
const BAR_COLOR = CHART_BAR_ACTIVE_BLUE;
@@ -51,6 +54,10 @@ const ClassificationDistributionCard = memo(({ tag,
isLoading }: ClassificationD
const navigate = useNavigate();
const data = getClassificationDistribution(tag, 5);
const associationTotal = useMemo(() =>
getTagEntityAssociationTotal(tag), [tag]);
+ const yAxisWidth = useMemo(
+ () => getClassificationYAxisWidth(data.map((item) =>
item.name)),
+ [data],
+ );
const handleBarClick = useCallback(
(entry: { name: string }) => {
@@ -155,18 +162,14 @@ const ClassificationDistributionCard = memo(({ tag,
isLoading }: ClassificationD
<YAxis
type="category"
dataKey="name"
- width={52}
- label={{
- value:
"Classification",
- angle:
-90,
-
position: "left",
- offset:
2,
- style:
{ fontSize: 10, fill: "#6c757d", textAnchor: "middle" },
- }}
+
width={yAxisWidth}
+ tickMargin={4}
tick={(props:
Record<string, unknown>) => {
const {
x = 0, y = 0, payload } = props;
const p
= payload as { value?: string; name?: string } | undefined;
const
value = p?.value ?? p?.name ?? (typeof payload === "string" ? payload : "");
+ const
displayLabel = truncateClassificationYAxisLabel(value);
+ const
isTruncated = isClassificationYAxisLabelTruncated(value);
return (
<g
transform={`translate(${x},${y})`}
@@ -174,6 +177,7 @@ const ClassificationDistributionCard = memo(({ tag,
isLoading }: ClassificationD
style={{ cursor: value ? "pointer" : "default" }}
role={value ? "button" : undefined}
tabIndex={value ? 0 : undefined}
+
aria-label={value || undefined}
onKeyDown={
value
? (e: React.KeyboardEvent<SVGGElement>) => {
@@ -185,8 +189,9 @@ const ClassificationDistributionCard = memo(({ tag,
isLoading }: ClassificationD
: undefined
}
>
+
{isTruncated ? <title>{value}</title> : null}
<text x={0} y={0} dy={4} textAnchor="end" fill="#333" fontSize={12}>
-
{value}
+
{displayLabel}
</text>
</g>
);
diff --git
a/dashboard/src/views/DashboardOverview/__tests__/ClassificationDistributionCard.test.tsx
b/dashboard/src/views/DashboardOverview/__tests__/ClassificationDistributionCard.test.tsx
new file mode 100644
index 000000000..8e423c160
--- /dev/null
+++
b/dashboard/src/views/DashboardOverview/__tests__/ClassificationDistributionCard.test.tsx
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 React from 'react';
+import { render, screen } from '@testing-library/react';
+import { MemoryRouter } from 'react-router-dom';
+import ClassificationDistributionCard from '../ClassificationDistributionCard';
+import {
+ CLASSIFICATION_Y_AXIS_LABEL_MAX_LENGTH,
+ CLASSIFICATION_Y_AXIS_LABEL_SUFFIX,
+} from '../dashboardChartPalette';
+
+jest.mock('@utils/Helper', () => ({
+ numberFormatWithComma: (n: number | string) => String(n),
+}));
+
+const mockNavigateToSearch = jest.fn();
+const mockNavigateToClassificationSearch = jest.fn();
+jest.mock('@utils/dashboardSearchUtils', () => ({
+ navigateToSearch: (...args: unknown[]) => mockNavigateToSearch(...args),
+ navigateToClassificationSearch: (...args: unknown[]) =>
+ mockNavigateToClassificationSearch(...args),
+}));
+
+const shortName = 'PII';
+const longName = 'a'.repeat(CLASSIFICATION_Y_AXIS_LABEL_MAX_LENGTH + 10);
+const truncatedLongName =
`${'a'.repeat(CLASSIFICATION_Y_AXIS_LABEL_MAX_LENGTH)}${CLASSIFICATION_Y_AXIS_LABEL_SUFFIX}`;
+
+jest.mock('@utils/metricsUtils', () => ({
+ getClassificationDistribution: jest.fn(() => [
+ { name: shortName, count: 12 },
+ { name: longName, count: 8 },
+ ]),
+ getTagEntityAssociationTotal: jest.fn(() => 20),
+}));
+
+jest.mock('recharts', () => ({
+ ResponsiveContainer: ({ children }: { children?: React.ReactNode }) => (
+ <div data-testid="rc">{children}</div>
+ ),
+ BarChart: ({ children }: { children?: React.ReactNode }) => (
+ <div data-testid="bar-chart">{children}</div>
+ ),
+ CartesianGrid: () => <div data-testid="grid" />,
+ XAxis: () => <div data-testid="x-axis" />,
+ YAxis: ({ tick }: { tick?: React.ComponentType<Record<string, unknown>>
}) => {
+ const Tick = tick;
+ if (!Tick) return null;
+ return (
+ <div data-testid="y-axis">
+ <Tick x={10} y={20} payload={{ value: shortName
}} />
+ <Tick x={10} y={40} payload={{ value: longName
}} />
+ </div>
+ );
+ },
+ Tooltip: () => <div data-testid="tooltip-mock" />,
+ Bar: ({ children }: { children?: React.ReactNode }) => (
+ <div data-testid="bar">{children}</div>
+ ),
+ Cell: () => <div data-testid="cell" />,
+ LabelList: () => <div data-testid="label-list" />,
+}));
+
+describe('ClassificationDistributionCard', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders short Y-axis labels without truncation or title tooltip',
() => {
+ render(
+ <MemoryRouter>
+ <ClassificationDistributionCard tag={{}} />
+ </MemoryRouter>,
+ );
+
+ expect(screen.getByText(shortName)).toBeInTheDocument();
+ const shortLabelGroup =
screen.getByText(shortName).closest('g');
+ expect(shortLabelGroup?.querySelector('title')).toBeNull();
+ });
+
+ it('truncates long Y-axis labels and exposes full name in SVG title
tooltip', () => {
+ render(
+ <MemoryRouter>
+ <ClassificationDistributionCard tag={{}} />
+ </MemoryRouter>,
+ );
+
+ expect(screen.getByText(truncatedLongName)).toBeInTheDocument();
+
+ const truncatedLabelGroup =
screen.getByText(truncatedLongName).closest('g');
+ const titleNode = truncatedLabelGroup?.querySelector('title');
+ expect(titleNode).not.toBeNull();
+ expect(titleNode?.textContent).toBe(longName);
+
+ const visibleTextNodes =
truncatedLabelGroup?.querySelectorAll('text');
+ expect(visibleTextNodes?.length).toBe(1);
+
expect(visibleTextNodes?.[0]?.textContent).toBe(truncatedLongName);
+ });
+});
diff --git
a/dashboard/src/views/DashboardOverview/__tests__/dashboardChartPalette.test.ts
b/dashboard/src/views/DashboardOverview/__tests__/dashboardChartPalette.test.ts
new file mode 100644
index 000000000..11e22f99a
--- /dev/null
+++
b/dashboard/src/views/DashboardOverview/__tests__/dashboardChartPalette.test.ts
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 {
+ CLASSIFICATION_Y_AXIS_CHAR_WIDTH,
+ CLASSIFICATION_Y_AXIS_LABEL_MAX_LENGTH,
+ CLASSIFICATION_Y_AXIS_LABEL_SUFFIX,
+ CLASSIFICATION_Y_AXIS_MAX_WIDTH,
+ CLASSIFICATION_Y_AXIS_MIN_WIDTH,
+ getChartYAxisWidth,
+ getClassificationYAxisWidth,
+ isChartYAxisLabelTruncated,
+ isClassificationYAxisLabelTruncated,
+ truncateChartYAxisLabel,
+ truncateClassificationYAxisLabel,
+} from '../dashboardChartPalette';
+
+describe('dashboardChartPalette', () => {
+ describe('truncateClassificationYAxisLabel', () => {
+ it('returns the label unchanged when within max length', () => {
+
expect(truncateClassificationYAxisLabel('PII')).toBe('PII');
+
expect(truncateClassificationYAxisLabel('a'.repeat(30))).toBe(
+ 'a'.repeat(30),
+ );
+ });
+
+ it('truncates to 30 characters and appends ellipsis', () => {
+ const fullName = 'a'.repeat(45);
+ expect(truncateClassificationYAxisLabel(fullName)).toBe(
+
`${'a'.repeat(CLASSIFICATION_Y_AXIS_LABEL_MAX_LENGTH)}${CLASSIFICATION_Y_AXIS_LABEL_SUFFIX}`,
+ );
+ });
+ });
+
+ describe('isClassificationYAxisLabelTruncated', () => {
+ it('returns false for labels at or below max length', () => {
+
expect(isClassificationYAxisLabelTruncated('PII')).toBe(false);
+
expect(isClassificationYAxisLabelTruncated('a'.repeat(30))).toBe(false);
+ });
+
+ it('returns true for labels longer than max length', () => {
+
expect(isClassificationYAxisLabelTruncated('a'.repeat(31))).toBe(true);
+ });
+ });
+
+ describe('getClassificationYAxisWidth', () => {
+ it('returns minimum width when labels are empty', () => {
+ expect(getClassificationYAxisWidth([])).toBe(
+ CLASSIFICATION_Y_AXIS_MIN_WIDTH,
+ );
+ });
+
+ it('returns minimum width for short classification names', ()
=> {
+ expect(getClassificationYAxisWidth(['PII',
'HIPAA'])).toBe(
+ CLASSIFICATION_Y_AXIS_MIN_WIDTH,
+ );
+ });
+
+ it('scales width from truncated display length', () => {
+ const longLabel = 'a'.repeat(25);
+ expect(getClassificationYAxisWidth(['PII',
longLabel])).toBe(
+ 25 * CLASSIFICATION_Y_AXIS_CHAR_WIDTH,
+ );
+ });
+
+ it('uses truncated length cap for very long classification
names', () => {
+ const veryLongLabel = 'a'.repeat(80);
+ const truncatedLength =
+ CLASSIFICATION_Y_AXIS_LABEL_MAX_LENGTH +
+ CLASSIFICATION_Y_AXIS_LABEL_SUFFIX.length;
+
expect(getClassificationYAxisWidth([veryLongLabel])).toBe(
+ truncatedLength *
CLASSIFICATION_Y_AXIS_CHAR_WIDTH,
+ );
+ });
+
+ it('does not exceed maximum width', () => {
+ const labels = Array.from({ length: 10 }, () =>
'x'.repeat(80));
+
expect(getClassificationYAxisWidth(labels)).toBeLessThanOrEqual(
+ CLASSIFICATION_Y_AXIS_MAX_WIDTH,
+ );
+ });
+ });
+
+ describe('shared chart Y-axis aliases', () => {
+ it('exposes the same helpers for service type and
classification charts', () => {
+ const label = 'a'.repeat(45);
+ expect(truncateChartYAxisLabel(label)).toBe(
+ truncateClassificationYAxisLabel(label),
+ );
+ expect(isChartYAxisLabelTruncated(label)).toBe(
+ isClassificationYAxisLabelTruncated(label),
+ );
+ expect(getChartYAxisWidth([label])).toBe(
+ getClassificationYAxisWidth([label]),
+ );
+ });
+ });
+});
diff --git a/dashboard/src/views/DashboardOverview/dashboardChartPalette.ts
b/dashboard/src/views/DashboardOverview/dashboardChartPalette.ts
index fb012a445..ec6c03ea7 100644
--- a/dashboard/src/views/DashboardOverview/dashboardChartPalette.ts
+++ b/dashboard/src/views/DashboardOverview/dashboardChartPalette.ts
@@ -33,10 +33,44 @@ export const HORIZONTAL_BAR_CHART_MARGIN = {
bottom: 48,
} as const;
-/** Tighter layout: short classification names — minimize dead space left of Y
ticks */
+/** Classification bar chart: Y-axis width reserves label space; keep left
margin minimal */
export const CLASSIFICATION_DISTRIBUTION_CHART_MARGIN = {
top: 8,
right: 72,
- left: 12,
+ left: 8,
bottom: 48,
} as const;
+
+export const CLASSIFICATION_Y_AXIS_MIN_WIDTH = 160;
+export const CLASSIFICATION_Y_AXIS_MAX_WIDTH = 360;
+export const CLASSIFICATION_Y_AXIS_CHAR_WIDTH = 8;
+export const CLASSIFICATION_Y_AXIS_LABEL_MAX_LENGTH = 30;
+export const CLASSIFICATION_Y_AXIS_LABEL_SUFFIX = '...';
+
+/** Truncate Y-axis classification labels for display (full name shown via SVG
title on hover). */
+export const truncateClassificationYAxisLabel = (label: string): string => {
+ if (label.length <= CLASSIFICATION_Y_AXIS_LABEL_MAX_LENGTH) {
+ return label;
+ }
+ return `${label.slice(0,
CLASSIFICATION_Y_AXIS_LABEL_MAX_LENGTH)}${CLASSIFICATION_Y_AXIS_LABEL_SUFFIX}`;
+};
+
+export const isClassificationYAxisLabelTruncated = (label: string): boolean =>
+ label.length > CLASSIFICATION_Y_AXIS_LABEL_MAX_LENGTH;
+
+/** Estimate Y-axis width from truncated label length (12px font, end-anchored
ticks). */
+export const getClassificationYAxisWidth = (labels: string[]): number => {
+ if (!labels.length) return CLASSIFICATION_Y_AXIS_MIN_WIDTH;
+ const longest = Math.max(
+ ...labels.map((label) =>
truncateClassificationYAxisLabel(label).length),
+ );
+ return Math.min(
+ CLASSIFICATION_Y_AXIS_MAX_WIDTH,
+ Math.max(CLASSIFICATION_Y_AXIS_MIN_WIDTH, longest *
CLASSIFICATION_Y_AXIS_CHAR_WIDTH),
+ );
+};
+
+/** Shared by classification and service type distribution chart Y-axis ticks
*/
+export const truncateChartYAxisLabel = truncateClassificationYAxisLabel;
+export const isChartYAxisLabelTruncated = isClassificationYAxisLabelTruncated;
+export const getChartYAxisWidth = getClassificationYAxisWidth;