diff --git a/.github/playwright/impact-map.generated.json b/.github/playwright/impact-map.generated.json
index d2a6dab48ee5..65a86538ff99 100644
--- a/.github/playwright/impact-map.generated.json
+++ b/.github/playwright/impact-map.generated.json
@@ -143,6 +143,7 @@
"playwright/e2e/Features/DataQuality/DataQualityDashboard.spec.ts",
"playwright/e2e/Features/DataQuality/DataQualityPermissions.spec.ts",
"playwright/e2e/Features/DataQuality/Dimensionality.spec.ts",
+ "playwright/e2e/Features/DataQuality/FailureThreshold.spec.ts",
"playwright/e2e/Features/DataQuality/IncidentManagerDateFilter.spec.ts",
"playwright/e2e/Features/DataQuality/IncidentManagerSqlQuery.spec.ts",
"playwright/e2e/Features/DataQuality/Profiler.spec.ts",
@@ -749,6 +750,7 @@
"playwright/e2e/Features/AppMode/AppModeUrlHandling.spec.ts",
"playwright/e2e/Features/ApplicationsSettingsModal.spec.ts",
"playwright/e2e/Features/CustomMetric.spec.ts",
+ "playwright/e2e/Features/DataQuality/FailureThreshold.spec.ts",
"playwright/e2e/Features/DataQuality/IncidentManagerDateFilter.spec.ts",
"playwright/e2e/Features/DataQuality/IncidentManagerSqlQuery.spec.ts",
"playwright/e2e/Features/DataQuality/RunTestCase.spec.ts",
@@ -802,6 +804,7 @@
"playwright/e2e/Features/DataQuality/AddTestCaseNewFlow.spec.ts",
"playwright/e2e/Features/DataQuality/BundleSuiteBulkOperations.spec.ts",
"playwright/e2e/Features/DataQuality/DataQuality.spec.ts",
+ "playwright/e2e/Features/DataQuality/FailureThreshold.spec.ts",
"playwright/e2e/Features/DataQuality/IncidentManagerDateFilter.spec.ts",
"playwright/e2e/Features/DataQuality/IncidentManagerSqlQuery.spec.ts",
"playwright/e2e/Features/DataQuality/Profiler.spec.ts",
@@ -1685,6 +1688,7 @@
"playwright/e2e/Features/DataQuality/DataQualityListAfterReindex.spec.ts",
"playwright/e2e/Features/DataQuality/DataQualityPermissions.spec.ts",
"playwright/e2e/Features/DataQuality/Dimensionality.spec.ts",
+ "playwright/e2e/Features/DataQuality/FailureThreshold.spec.ts",
"playwright/e2e/Features/DataQuality/IncidentManagerAfterOwnerChange.spec.ts",
"playwright/e2e/Features/DataQuality/IncidentManagerAfterReindex.spec.ts",
"playwright/e2e/Features/DataQuality/IncidentManagerAfterSoftDelete.spec.ts",
@@ -3347,6 +3351,7 @@
"openmetadata-ui/src/main/resources/ui/playwright/utils/codeEditor.ts"
],
"specs": [
+ "playwright/e2e/Features/DataQuality/FailureThreshold.spec.ts",
"playwright/e2e/Features/DataQuality/Profiler.spec.ts",
"playwright/e2e/Features/DataQuality/TableLevelTests.spec.ts",
"playwright/e2e/Features/Permission.spec.ts",
@@ -3448,6 +3453,7 @@
"playwright/e2e/Features/DataQuality/DataQualityListAfterReindex.spec.ts",
"playwright/e2e/Features/DataQuality/DataQualityPermissions.spec.ts",
"playwright/e2e/Features/DataQuality/Dimensionality.spec.ts",
+ "playwright/e2e/Features/DataQuality/FailureThreshold.spec.ts",
"playwright/e2e/Features/DataQuality/IncidentManagerAfterOwnerChange.spec.ts",
"playwright/e2e/Features/DataQuality/IncidentManagerAfterReindex.spec.ts",
"playwright/e2e/Features/DataQuality/IncidentManagerAfterSoftDelete.spec.ts",
@@ -3880,6 +3886,7 @@
"playwright/e2e/Features/DataQuality/DataQualityDashboard.spec.ts",
"playwright/e2e/Features/DataQuality/DataQualityDashboardAfterReindex.spec.ts",
"playwright/e2e/Features/DataQuality/Dimensionality.spec.ts",
+ "playwright/e2e/Features/DataQuality/FailureThreshold.spec.ts",
"playwright/e2e/Features/DataQuality/TableLevelTests.spec.ts",
"playwright/e2e/Features/IncidentManager.spec.ts",
"playwright/e2e/Features/TestSuiteMultiPipeline.spec.ts",
@@ -5120,6 +5127,7 @@
"playwright/e2e/Features/DataQuality/DataQualityDashboard.spec.ts",
"playwright/e2e/Features/DataQuality/DataQualityPermissions.spec.ts",
"playwright/e2e/Features/DataQuality/Dimensionality.spec.ts",
+ "playwright/e2e/Features/DataQuality/FailureThreshold.spec.ts",
"playwright/e2e/Features/DataQuality/Profiler.spec.ts",
"playwright/e2e/Features/DataQuality/RunTestCase.spec.ts",
"playwright/e2e/Features/DataQuality/TableLevelTests.spec.ts",
@@ -6384,6 +6392,7 @@
"playwright/e2e/Features/DataQuality/ColumnLevelTests.spec.ts",
"playwright/e2e/Features/DataQuality/DataQuality.spec.ts",
"playwright/e2e/Features/DataQuality/Dimensionality.spec.ts",
+ "playwright/e2e/Features/DataQuality/FailureThreshold.spec.ts",
"playwright/e2e/Features/DataQuality/TableLevelTests.spec.ts",
"playwright/e2e/Pages/DataContracts.spec.ts",
"playwright/e2e/VersionPages/TestCaseVersionPage.spec.ts"
@@ -6524,6 +6533,7 @@
"openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/RunDetailsCard/RunDetailsCard.tsx"
],
"specs": [
+ "playwright/e2e/Features/DataQuality/FailureThreshold.spec.ts",
"playwright/e2e/Features/DataQuality/TestCaseDetailsPage.spec.ts"
]
},
@@ -6798,6 +6808,7 @@
"openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaEditor/CodeEditor.tsx"
],
"specs": [
+ "playwright/e2e/Features/DataQuality/FailureThreshold.spec.ts",
"playwright/e2e/Features/DataQuality/TableLevelTests.spec.ts",
"playwright/e2e/Features/QueryEntity.spec.ts",
"playwright/e2e/Pages/DataContracts.spec.ts"
@@ -6808,6 +6819,7 @@
"openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaEditor/SchemaEditor.tsx"
],
"specs": [
+ "playwright/e2e/Features/DataQuality/FailureThreshold.spec.ts",
"playwright/e2e/Features/DataQuality/TableLevelTests.spec.ts",
"playwright/e2e/Features/QueryEntity.spec.ts",
"playwright/e2e/Pages/DataContracts.spec.ts"
@@ -11320,6 +11332,14 @@
"playwright/e2e/Features/DataQuality/DataQualityDashboard.spec.ts"
]
},
+ {
+ "sources": [
+ "openmetadata-ui/src/main/resources/ui/src/components/observability/data-quality/ThresholdPreview/ThresholdPreview.tsx"
+ ],
+ "specs": [
+ "playwright/e2e/Features/DataQuality/FailureThreshold.spec.ts"
+ ]
+ },
{
"sources": [
"openmetadata-ui/src/main/resources/ui/src/components/platform/ai-shell/AINotFoundPage/AINotFoundPage.tsx"
@@ -11876,6 +11896,7 @@
"playwright/e2e/Features/DataQuality/DataQualityListAfterReindex.spec.ts",
"playwright/e2e/Features/DataQuality/DataQualityPermissions.spec.ts",
"playwright/e2e/Features/DataQuality/Dimensionality.spec.ts",
+ "playwright/e2e/Features/DataQuality/FailureThreshold.spec.ts",
"playwright/e2e/Features/DataQuality/IncidentManagerAfterOwnerChange.spec.ts",
"playwright/e2e/Features/DataQuality/IncidentManagerAfterReindex.spec.ts",
"playwright/e2e/Features/DataQuality/IncidentManagerAfterSoftDelete.spec.ts",
diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/app-navigation/base-components/nav-item.test.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/app-navigation/base-components/nav-item.test.tsx
new file mode 100644
index 000000000000..60242778ebb6
--- /dev/null
+++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/app-navigation/base-components/nav-item.test.tsx
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2026 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { render, screen } from '@testing-library/react';
+import { describe, expect, it } from 'vitest';
+import { NavItemBase, type NavItemSize } from './nav-item';
+
+const Icon = () => ;
+
+describe('NavItemBase', () => {
+ it.each<[NavItemSize, string]>([
+ ['sm', 'tw:pl-9'],
+ ['md', 'tw:pl-10'],
+ ])(
+ 'indents a %s collapsible child to line up with its parent label',
+ (size, indent) => {
+ render(
+
+ Child
+
+ );
+
+ expect(screen.getByRole('link', { name: 'Child' })).toHaveClass(indent);
+ }
+ );
+
+ it.each<[NavItemSize, number]>([
+ ['md', 0],
+ ['lg', 1],
+ ])('shows a %s collapsible child icon %i time(s)', (size, count) => {
+ render(
+
+ Child
+
+ );
+
+ expect(screen.queryAllByTestId('nav-icon')).toHaveLength(count);
+ });
+
+ it('fills the current item with the brand colour in the solid variant', () => {
+ render(
+ <>
+
+ Current
+
+
+ Default
+
+
+ Resting
+
+ >
+ );
+
+ expect(screen.getByRole('link', { name: 'Resting' })).toHaveClass(
+ 'tw:bg-transparent'
+ );
+ expect(screen.getByText('Resting')).toHaveClass('tw:text-tertiary');
+ expect(screen.getByText('Current')).toHaveClass('tw:text-primary_on-brand');
+
+ expect(screen.getByRole('link', { name: 'Current' })).toHaveClass(
+ 'tw:bg-brand-solid'
+ );
+ expect(screen.getByRole('link', { name: 'Default' })).toHaveClass(
+ 'tw:bg-brand-primary_alt'
+ );
+ });
+});
diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/app-navigation/base-components/nav-item.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/app-navigation/base-components/nav-item.tsx
index 21805ae12670..a948c7d0ef08 100644
--- a/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/app-navigation/base-components/nav-item.tsx
+++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/app-navigation/base-components/nav-item.tsx
@@ -14,26 +14,64 @@ const styles = sortCx({
rootSelected: 'tw:bg-brand-primary_alt tw:hover:bg-brand-primary_alt',
});
-export type NavItemSize = 'sm' | 'md';
+export type NavItemVariant = 'default' | 'solid';
+
+// `solid` fills the current item with the brand colour and opens a collapsible
+// group under a filled header; hover tints rest items brand instead of gray.
+const solidStyles = sortCx({
+ // Rest items sit on the sidebar surface instead of painting white boxes.
+ root: 'tw:bg-transparent tw:hover:bg-brand-primary_alt tw:dark:hover:bg-brand-primary_alt',
+ rootSelected:
+ 'tw:bg-brand-solid tw:hover:bg-brand-solid tw:dark:bg-brand-solid tw:dark:hover:bg-brand-solid',
+ groupSelected:
+ 'tw:bg-brand-solid_hover tw:hover:bg-brand-solid_hover tw:dark:bg-brand-solid_hover tw:dark:hover:bg-brand-solid_hover',
+ groupOpen:
+ 'tw:in-open:rounded-b-none tw:in-open:bg-brand-solid_hover tw:in-open:hover:bg-brand-solid_hover tw:dark:in-open:bg-brand-solid_hover',
+ // Rest label and icon share one mid-gray, as in the pre-migration classic sidebar.
+ label: 'tw:text-tertiary tw:group-hover:text-brand-secondary',
+ labelSelected:
+ 'tw:text-primary_on-brand tw:group-hover:text-primary_on-brand',
+ labelOpen:
+ 'tw:in-open:text-primary_on-brand tw:in-open:group-hover:text-primary_on-brand',
+ icon: 'tw:text-fg-tertiary tw:group-hover:text-fg-brand-secondary_alt',
+ iconSelected: 'tw:text-fg-white tw:group-hover:text-fg-white',
+ iconOpen: 'tw:in-open:text-fg-white tw:in-open:group-hover:text-fg-white',
+});
+
+export type NavItemSize = 'sm' | 'md' | 'lg';
const sizeStyles = sortCx({
sm: {
- padding: 'tw:py-1',
- icon: 'tw:size-4',
+ root: 'tw:px-3 tw:py-1',
+ // Lines child labels up with the parent label: px-3 + icon + mr-2.
+ child: 'tw:py-1 tw:pr-3 tw:pl-9',
+ icon: 'tw:mr-2 tw:size-4',
label: 'tw:text-sm tw:font-normal',
labelCurrent: 'tw:font-semibold',
},
md: {
- padding: 'tw:py-2',
- icon: 'tw:size-5',
+ root: 'tw:px-3 tw:py-2',
+ child: 'tw:py-2 tw:pr-3 tw:pl-10',
+ icon: 'tw:mr-2 tw:size-5',
label: 'tw:text-md tw:font-semibold',
labelCurrent: '',
},
+ // Roomy primary navigation: 56px rows with 28px icons. Children keep their
+ // icon and sit flush inside the open group.
+ lg: {
+ root: 'tw:rounded-xl tw:p-3.5',
+ child: 'tw:rounded-none tw:px-4 tw:py-2.5',
+ icon: 'tw:mr-2.5 tw:size-7',
+ label: 'tw:text-sm tw:font-medium',
+ labelCurrent: 'tw:font-semibold',
+ },
});
interface NavItemBaseProps {
- /** Text and icon size. `sm` is for dense secondary lists. Defaults to `md`. */
+ /** Text and icon size. `sm` is for dense secondary lists, `lg` for roomy primary navigation. Defaults to `md`. */
size?: NavItemSize;
+ /** `solid` fills the current item and an open group's header with the brand colour. Defaults to `default`. */
+ variant?: NavItemVariant;
/** Whether the nav item shows only an icon. */
iconOnly?: boolean;
/** Whether the collapsible nav item is open. */
@@ -67,20 +105,42 @@ export const NavItemBase = ({
children,
truncate = true,
size = 'md',
+ variant = 'default',
dataTestId,
onClick,
}: NavItemBaseProps) => {
- const iconElement = Icon && (
+ const isSolid = variant === 'solid';
+ const isGroup = type === 'collapsible';
+ // Children only show their icon at `lg`; smaller sizes indent instead.
+ const showIcon = type !== 'collapsible-child' || size === 'lg';
+
+ const iconElement = Icon && showIcon && (
);
+ const rootClassName = cx(
+ styles.root,
+ type === 'collapsible-child'
+ ? sizeStyles[size].child
+ : sizeStyles[size].root,
+ current && styles.rootSelected,
+ isSolid && solidStyles.root,
+ isSolid &&
+ current &&
+ (isGroup ? solidStyles.groupSelected : solidStyles.rootSelected),
+ isSolid && isGroup && solidStyles.groupOpen
+ );
+
const badgeElement =
badge && (typeof badge === 'string' || typeof badge === 'number') ? (
@@ -97,7 +157,11 @@ export const NavItemBase = ({
sizeStyles[size].label,
current && sizeStyles[size].labelCurrent,
truncate && 'tw:truncate',
- current && 'tw:text-brand-secondary tw:group-hover:text-brand-secondary'
+ current &&
+ 'tw:text-brand-secondary tw:group-hover:text-brand-secondary',
+ isSolid && solidStyles.label,
+ isSolid && current && solidStyles.labelSelected,
+ isSolid && isGroup && solidStyles.labelOpen
)}>
{children}
@@ -110,14 +174,7 @@ export const NavItemBase = ({
if (type === 'collapsible') {
return (
-
+
{iconElement}
{labelElement}
@@ -126,7 +183,11 @@ export const NavItemBase = ({
);
@@ -136,17 +197,13 @@ export const NavItemBase = ({
return (
+ {iconElement}
{labelElement}
{externalIcon}
{badgeElement}
@@ -157,12 +214,7 @@ export const NavItemBase = ({
return (
{
};
const renderLeaf = (item: SidebarEntry, parentKey?: string) => {
- if (!item.redirect_url) {
+ const handleSelect = () => {
+ navigateToItem(item, parentKey);
+ if (item.key === SidebarItem.LOGOUT) {
+ handleLogoutClick();
+ }
+ };
+
+ if (isSidebarCollapsed && !item.redirect_url) {
return (
+ onPress={handleSelect}
+ />
);
}
@@ -153,9 +154,11 @@ const LeftSidebar = () => {
dataTestId={item.dataTestId}
href={item.redirect_url}
icon={item.icon}
+ size="lg"
truncate={false}
type={parentKey ? 'collapsible-child' : 'link'}
- onClick={() => navigateToItem(item, parentKey)}>
+ variant="solid"
+ onClick={handleSelect}>
{t(item.title)}
);
@@ -233,6 +236,7 @@ const LeftSidebar = () => {
return (
toggleGroup(item.key, event.currentTarget.open)}>
{
selectedKeys.includes(child.key)
)}
icon={item.icon}
+ size="lg"
truncate={false}
- type="collapsible">
+ type="collapsible"
+ variant="solid">
{t(item.title)}
{item.children?.map((child) => (
- -
+
-
{renderLeaf(child, item.key)}
))}
@@ -260,7 +263,7 @@ const LeftSidebar = () => {
return (