diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts
index 7a290862369..9f1d8bc493f 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts
@@ -1262,6 +1262,10 @@ test.describe('Glossary tests', () => {
const checkboxLabels = ['Reviewer', 'Synonyms'];
await selectColumns(page, checkboxLabels);
await verifyColumnsVisibility(page, checkboxLabels, true);
+
+ await page.reload();
+ await page.waitForLoadState('networkidle');
+ await verifyColumnsVisibility(page, checkboxLabels, true);
}
);
@@ -1272,6 +1276,10 @@ test.describe('Glossary tests', () => {
const checkboxLabels = ['Reviewer', 'Owners'];
await deselectColumns(page, checkboxLabels);
await verifyColumnsVisibility(page, checkboxLabels, false);
+
+ await page.reload();
+ await page.waitForLoadState('networkidle');
+ await verifyColumnsVisibility(page, checkboxLabels, false);
}
);
@@ -1287,6 +1295,10 @@ test.describe('Glossary tests', () => {
'ACTIONS',
];
await verifyAllColumns(page, tableColumns, true);
+
+ await page.reload();
+ await page.waitForLoadState('networkidle');
+ await verifyAllColumns(page, tableColumns, true);
});
await test.step('Hide All columns selection', async () => {
@@ -1299,6 +1311,10 @@ test.describe('Glossary tests', () => {
'STATUS',
];
await verifyAllColumns(page, tableColumns, false);
+
+ await page.reload();
+ await page.waitForLoadState('networkidle');
+ await verifyAllColumns(page, tableColumns, false);
});
} finally {
await glossaryTerm1.delete(apiContext);
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Table/Table.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/Table/Table.test.tsx
index 05451efca46..1c41207ec26 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/Table/Table.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Table/Table.test.tsx
@@ -11,7 +11,7 @@
* limitations under the License.
*/
-import { render, screen } from '@testing-library/react';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { getCustomizeColumnDetails } from '../../../utils/CustomizeColumnUtils';
@@ -25,10 +25,50 @@ jest.mock('../../../utils/CustomizeColumnUtils', () => ({
getReorderedColumns: jest.fn().mockImplementation((_, columns) => columns),
}));
+jest.mock('../../../utils/TableUtils', () => ({
+ getTableExpandableConfig: jest.fn(),
+}));
+
jest.mock('../SearchBarComponent/SearchBar.component', () =>
jest.fn().mockImplementation(() =>
SearchBar
)
);
+// Mock DraggableMenuItem component
+jest.mock('./DraggableMenu/DraggableMenuItem.component', () =>
+ jest.fn().mockImplementation(({ currentItem, selectedOptions, onSelect }) => (
+
+ onSelect(currentItem.value, e.target.checked)}
+ />
+
+
+ ))
+);
+
+// Mock hooks
+const mockSetPreference = jest.fn();
+const mockUseCurrentUserPreferences = {
+ preferences: {
+ selectedEntityTableColumns: {},
+ },
+ setPreference: mockSetPreference,
+};
+
+const mockUseGenericContext = {
+ type: 'table',
+};
+
+jest.mock('../../../hooks/currentUserStore/useCurrentUserStore', () => ({
+ useCurrentUserPreferences: jest.fn(() => mockUseCurrentUserPreferences),
+}));
+
+jest.mock('../../Customization/GenericProvider/GenericProvider', () => ({
+ useGenericContext: jest.fn(() => mockUseGenericContext),
+}));
+
const mockColumns = [
{
title: 'Column 1',
@@ -40,11 +80,16 @@ const mockColumns = [
dataIndex: 'col2',
key: 'col2',
},
+ {
+ title: 'Column 3',
+ dataIndex: 'col3',
+ key: 'col3',
+ },
];
const mockData = [
- { col1: 'Value 1', col2: 'Value 2' },
- { col1: 'Value 3', col2: 'Value 4' },
+ { col1: 'Value 1', col2: 'Value 2', col3: 'Value 3' },
+ { col1: 'Value 4', col2: 'Value 5', col3: 'Value 6' },
];
describe('Table component', () => {
@@ -56,6 +101,11 @@ describe('Table component', () => {
);
};
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUseCurrentUserPreferences.preferences.selectedEntityTableColumns = {};
+ });
+
it('should display skeleton loader if loading is true', async () => {
renderComponent({ loading: true });
@@ -105,4 +155,317 @@ describe('Table component', () => {
expect(screen.getByTestId('table-filters')).toBeInTheDocument();
});
+
+ describe('Column Selection Functionality', () => {
+ beforeEach(() => {
+ (getCustomizeColumnDetails as jest.Mock).mockReturnValue([
+ { label: 'Column 1', value: 'col1' },
+ { label: 'Column 2', value: 'col2' },
+ { label: 'Column 3', value: 'col3' },
+ ]);
+ });
+
+ it('should initialize column selections from existing user preferences', () => {
+ mockUseCurrentUserPreferences.preferences.selectedEntityTableColumns = {
+ table: ['col1', 'col2'],
+ };
+
+ renderComponent({
+ staticVisibleColumns: ['col1'],
+ defaultVisibleColumns: ['col2', 'col3'],
+ entityType: 'table',
+ });
+
+ // Component should use existing preferences
+ expect(mockSetPreference).not.toHaveBeenCalled();
+ });
+
+ it('should use default columns when no existing preferences and customization is enabled', () => {
+ mockUseCurrentUserPreferences.preferences.selectedEntityTableColumns = {};
+
+ renderComponent({
+ staticVisibleColumns: ['col1'],
+ defaultVisibleColumns: ['col2', 'col3'],
+ entityType: 'table',
+ });
+
+ // Component should not automatically set preferences
+ expect(mockSetPreference).not.toHaveBeenCalled();
+ });
+
+ it('should require both static and default columns for customization', () => {
+ renderComponent({
+ staticVisibleColumns: ['col1'],
+ defaultVisibleColumns: ['col2'],
+ });
+
+ expect(screen.getByTestId('column-dropdown')).toBeInTheDocument();
+ });
+
+ it('should not render column dropdown when only staticVisibleColumns is provided', () => {
+ renderComponent({
+ staticVisibleColumns: ['col1'],
+ defaultVisibleColumns: undefined,
+ });
+
+ expect(screen.queryByTestId('column-dropdown')).not.toBeInTheDocument();
+ });
+
+ it('should not render column dropdown when only defaultVisibleColumns is provided', () => {
+ renderComponent({
+ staticVisibleColumns: undefined,
+ defaultVisibleColumns: ['col2'],
+ });
+
+ expect(screen.queryByTestId('column-dropdown')).not.toBeInTheDocument();
+ });
+
+ it('should not render column dropdown when both static and default columns are empty', () => {
+ renderComponent({
+ staticVisibleColumns: undefined,
+ defaultVisibleColumns: undefined,
+ });
+
+ expect(screen.queryByTestId('column-dropdown')).not.toBeInTheDocument();
+ });
+
+ it('should not enable customization when no static or default columns are provided', () => {
+ renderComponent();
+
+ expect(screen.queryByTestId('column-dropdown')).not.toBeInTheDocument();
+ });
+
+ it('should open column dropdown and show column options', async () => {
+ mockUseCurrentUserPreferences.preferences.selectedEntityTableColumns = {
+ table: ['col1', 'col2'],
+ };
+
+ renderComponent({
+ staticVisibleColumns: ['col1'],
+ defaultVisibleColumns: ['col2'],
+ });
+
+ const columnDropdown = screen.getByTestId('column-dropdown');
+ fireEvent.click(columnDropdown);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('column-dropdown-title')).toBeInTheDocument();
+ expect(screen.getByText('label.column')).toBeInTheDocument();
+ });
+ });
+
+ it('should handle column selection when checkbox is clicked', async () => {
+ mockUseCurrentUserPreferences.preferences.selectedEntityTableColumns = {
+ table: ['col1', 'col2'],
+ };
+
+ renderComponent({
+ staticVisibleColumns: ['col1'],
+ defaultVisibleColumns: ['col2'],
+ entityType: 'table',
+ });
+
+ const columnDropdown = screen.getByTestId('column-dropdown');
+ fireEvent.click(columnDropdown);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('column-checkbox-col2')).toBeInTheDocument();
+ });
+
+ const checkbox = screen.getByTestId('column-checkbox-col2');
+ fireEvent.click(checkbox);
+
+ // Verify that preferences are updated
+ expect(mockSetPreference).toHaveBeenCalledWith({
+ selectedEntityTableColumns: {
+ table: ['col1'],
+ },
+ });
+ });
+
+ it('should handle column addition when unchecked checkbox is clicked', async () => {
+ mockUseCurrentUserPreferences.preferences.selectedEntityTableColumns = {
+ table: ['col1'],
+ };
+
+ renderComponent({
+ staticVisibleColumns: ['col1'],
+ defaultVisibleColumns: ['col2', 'col3'],
+ entityType: 'table',
+ });
+
+ const columnDropdown = screen.getByTestId('column-dropdown');
+ fireEvent.click(columnDropdown);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('column-checkbox-col3')).toBeInTheDocument();
+ });
+
+ const checkbox = screen.getByTestId('column-checkbox-col3');
+ fireEvent.click(checkbox);
+
+ expect(mockSetPreference).toHaveBeenCalledWith({
+ selectedEntityTableColumns: {
+ table: ['col1', 'col3'],
+ },
+ });
+ });
+
+ it('should show "View All" button when not all columns are selected', async () => {
+ mockUseCurrentUserPreferences.preferences.selectedEntityTableColumns = {
+ table: ['col1'],
+ };
+
+ renderComponent({
+ staticVisibleColumns: ['col1'],
+ defaultVisibleColumns: ['col2'],
+ });
+
+ const columnDropdown = screen.getByTestId('column-dropdown');
+ fireEvent.click(columnDropdown);
+
+ await waitFor(() => {
+ expect(
+ screen.getByTestId('column-dropdown-action-button')
+ ).toBeInTheDocument();
+ expect(screen.getByText('label.view-all')).toBeInTheDocument();
+ });
+ });
+
+ it('should show "Hide All" button when all columns are selected', async () => {
+ mockUseCurrentUserPreferences.preferences.selectedEntityTableColumns = {
+ table: ['col1', 'col2', 'col3'],
+ };
+
+ renderComponent({
+ staticVisibleColumns: ['col1'],
+ defaultVisibleColumns: ['col2'],
+ });
+
+ const columnDropdown = screen.getByTestId('column-dropdown');
+ fireEvent.click(columnDropdown);
+
+ await waitFor(() => {
+ expect(
+ screen.getByTestId('column-dropdown-action-button')
+ ).toBeInTheDocument();
+ expect(screen.getByText('label.hide-all')).toBeInTheDocument();
+ });
+ });
+
+ it('should select all columns when "View All" button is clicked', async () => {
+ mockUseCurrentUserPreferences.preferences.selectedEntityTableColumns = {
+ table: ['col1'],
+ };
+
+ renderComponent({
+ staticVisibleColumns: ['col1'],
+ defaultVisibleColumns: ['col2'],
+ entityType: 'table',
+ });
+
+ const columnDropdown = screen.getByTestId('column-dropdown');
+ fireEvent.click(columnDropdown);
+
+ await waitFor(() => {
+ expect(
+ screen.getByTestId('column-dropdown-action-button')
+ ).toBeInTheDocument();
+ });
+
+ const viewAllButton = screen.getByTestId('column-dropdown-action-button');
+ fireEvent.click(viewAllButton);
+
+ expect(mockSetPreference).toHaveBeenCalledWith({
+ selectedEntityTableColumns: {
+ table: ['col1', 'col2', 'col3'],
+ },
+ });
+ });
+
+ it('should deselect all columns when "Hide All" button is clicked', async () => {
+ mockUseCurrentUserPreferences.preferences.selectedEntityTableColumns = {
+ table: ['col1', 'col2', 'col3'],
+ };
+
+ renderComponent({
+ staticVisibleColumns: ['col1'],
+ defaultVisibleColumns: ['col2'],
+ entityType: 'table',
+ });
+
+ const columnDropdown = screen.getByTestId('column-dropdown');
+ fireEvent.click(columnDropdown);
+
+ await waitFor(() => {
+ expect(
+ screen.getByTestId('column-dropdown-action-button')
+ ).toBeInTheDocument();
+ });
+
+ const hideAllButton = screen.getByTestId('column-dropdown-action-button');
+ fireEvent.click(hideAllButton);
+
+ expect(mockSetPreference).toHaveBeenCalledWith({
+ selectedEntityTableColumns: {
+ table: [],
+ },
+ });
+ });
+
+ it('should preserve existing preferences for other entity types', async () => {
+ mockUseCurrentUserPreferences.preferences.selectedEntityTableColumns = {
+ dashboard: ['dash1', 'dash2'],
+ table: ['col1'],
+ };
+
+ renderComponent({
+ staticVisibleColumns: ['col1'],
+ defaultVisibleColumns: ['col2'],
+ entityType: 'table',
+ });
+
+ const columnDropdown = screen.getByTestId('column-dropdown');
+ fireEvent.click(columnDropdown);
+
+ await waitFor(() => {
+ expect(
+ screen.getByTestId('column-dropdown-action-button')
+ ).toBeInTheDocument();
+ });
+
+ const viewAllButton = screen.getByTestId('column-dropdown-action-button');
+ fireEvent.click(viewAllButton);
+
+ expect(mockSetPreference).toHaveBeenCalledWith({
+ selectedEntityTableColumns: {
+ dashboard: ['dash1', 'dash2'],
+ table: ['col1', 'col2', 'col3'],
+ },
+ });
+ });
+
+ it('should render search bar when searchProps are provided', () => {
+ renderComponent({
+ staticVisibleColumns: ['col1'],
+ defaultVisibleColumns: ['col2'],
+ searchProps: {
+ placeholder: 'Search columns',
+ value: 'test',
+ onSearch: jest.fn(),
+ },
+ });
+
+ expect(screen.getByText('SearchBar')).toBeInTheDocument();
+ });
+
+ it('should not render column dropdown in full view mode', () => {
+ renderComponent({
+ staticVisibleColumns: undefined,
+ defaultVisibleColumns: undefined,
+ });
+
+ expect(screen.queryByTestId('column-dropdown')).not.toBeInTheDocument();
+ });
+ });
});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Table/Table.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/Table/Table.tsx
index 09afc2a4aad..776911096cd 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/Table/Table.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Table/Table.tsx
@@ -37,16 +37,12 @@ import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { useTranslation } from 'react-i18next';
import { ReactComponent as ColumnIcon } from '../../../assets/svg/ic-column.svg';
-import { useApplicationStore } from '../../../hooks/useApplicationStore';
+import { useCurrentUserPreferences } from '../../../hooks/currentUserStore/useCurrentUserStore';
import {
getCustomizeColumnDetails,
getReorderedColumns,
} from '../../../utils/CustomizeColumnUtils';
-import {
- getTableColumnConfigSelections,
- getTableExpandableConfig,
- handleUpdateTableColumnSelections,
-} from '../../../utils/TableUtils';
+import { getTableExpandableConfig } from '../../../utils/TableUtils';
import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider';
import Loader from '../Loader/Loader';
import NextPrevious from '../NextPrevious/NextPrevious';
@@ -73,7 +69,6 @@ const Table = >(
) => {
const { t } = useTranslation();
const { type } = useGenericContext();
- const { currentUser } = useApplicationStore();
const [propsColumns, setPropsColumns] = useState>([]);
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
const [dropdownColumnList, setDropdownColumnList] = useState<
@@ -86,6 +81,10 @@ const Table = >(
() => ({ columns: propsColumns as Column[], minWidth: 80 }),
[propsColumns]
);
+ const {
+ preferences: { selectedEntityTableColumns },
+ setPreference,
+ } = useCurrentUserPreferences();
const isLoading = useMemo(
() => (loading as SpinProps)?.spinning ?? (loading as boolean) ?? false,
@@ -94,9 +93,10 @@ const Table = >(
const entityKey = useMemo(() => entityType ?? type, [type, entityType]);
- // Check if the table is in Full View mode, if so, the dropdown and Customize Column feature is not available
- const isFullViewTable = useMemo(
- () => isEmpty(rest.staticVisibleColumns) && isEmpty(defaultVisibleColumns),
+ // Check if the table is customizable, if so, the dropdown and Customize Column feature is available
+ const isCustomizeColumnEnable = useMemo(
+ () =>
+ !isEmpty(rest.staticVisibleColumns) && !isEmpty(defaultVisibleColumns),
[rest.staticVisibleColumns, defaultVisibleColumns]
);
@@ -110,28 +110,47 @@ const Table = >(
const handleColumnItemSelect = useCallback(
(key: string, selected: boolean) => {
- const updatedSelections = handleUpdateTableColumnSelections(
- selected,
- key,
- columnDropdownSelections,
- currentUser?.fullyQualifiedName ?? '',
- entityKey
- );
+ const updatedSelections = selected
+ ? [...columnDropdownSelections, key]
+ : columnDropdownSelections.filter((item) => item !== key);
+
+ setPreference({
+ selectedEntityTableColumns: {
+ ...selectedEntityTableColumns,
+ [entityKey]: updatedSelections,
+ },
+ });
setColumnDropdownSelections(updatedSelections);
},
- [columnDropdownSelections, entityKey]
+ [columnDropdownSelections, selectedEntityTableColumns, entityKey]
);
const handleBulkColumnAction = useCallback(() => {
if (dropdownColumnList.length === columnDropdownSelections.length) {
setColumnDropdownSelections([]);
+ setPreference({
+ selectedEntityTableColumns: {
+ ...selectedEntityTableColumns,
+ [entityKey]: [],
+ },
+ });
} else {
- setColumnDropdownSelections(
- dropdownColumnList.map((option) => option.value)
- );
+ const columns = dropdownColumnList.map((option) => option.value);
+ setColumnDropdownSelections(columns);
+ setPreference({
+ selectedEntityTableColumns: {
+ ...selectedEntityTableColumns,
+ [entityKey]: columns,
+ },
+ });
}
- }, [dropdownColumnList, columnDropdownSelections]);
+ }, [
+ dropdownColumnList,
+ columnDropdownSelections,
+ selectedEntityTableColumns,
+ entityKey,
+ ]);
const menu = useMemo(
() => ({
@@ -203,15 +222,15 @@ const Table = >(
};
useEffect(() => {
- if (!isFullViewTable) {
+ if (isCustomizeColumnEnable) {
setDropdownColumnList(
getCustomizeColumnDetails(rest.columns, rest.staticVisibleColumns)
);
}
- }, [isFullViewTable, rest.columns, rest.staticVisibleColumns]);
+ }, [isCustomizeColumnEnable, rest.columns, rest.staticVisibleColumns]);
useEffect(() => {
- if (isFullViewTable) {
+ if (!isCustomizeColumnEnable) {
setPropsColumns(rest.columns ?? []);
} else {
const filteredColumns = (rest.columns ?? []).filter(
@@ -223,28 +242,31 @@ const Table = >(
setPropsColumns(getReorderedColumns(dropdownColumnList, filteredColumns));
}
}, [
- isFullViewTable,
+ isCustomizeColumnEnable,
rest.columns,
columnDropdownSelections,
rest.staticVisibleColumns,
]);
useEffect(() => {
- const selections = getTableColumnConfigSelections(
- currentUser?.fullyQualifiedName ?? '',
- entityKey,
- isFullViewTable,
- defaultVisibleColumns
- );
-
- setColumnDropdownSelections(selections);
- }, [entityKey, defaultVisibleColumns, isFullViewTable]);
+ if (isCustomizeColumnEnable) {
+ setColumnDropdownSelections(
+ selectedEntityTableColumns?.[entityKey] ?? defaultVisibleColumns ?? []
+ );
+ }
+ }, [
+ isCustomizeColumnEnable,
+ selectedEntityTableColumns,
+ entityKey,
+ defaultVisibleColumns,
+ ]);
return (
@@ -260,7 +282,7 @@ const Table = >(
/>
) : null}
- {(rest.extraTableFilters || !isFullViewTable) && (
+ {(rest.extraTableFilters || isCustomizeColumnEnable) && (
>(
)}
span={searchProps ? 12 : 24}>
{rest.extraTableFilters}
- {!isFullViewTable && (
+ {isCustomizeColumnEnable && (
;
}
interface Store {
@@ -31,6 +32,7 @@ interface Store {
const defaultPreferences: UserPreferences = {
isSidebarCollapsed: false,
+ selectedEntityTableColumns: {},
// Add default values for other preferences
};
diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx
index cab6d8a2bc9..4fa7a98db00 100644
--- a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx
@@ -743,66 +743,6 @@ export const updateFieldDescription = (
});
};
-export const getTableColumnConfigSelections = (
- userFqn: string,
- entityType: string | undefined,
- isFullViewTable: boolean,
- defaultColumns: string[] | undefined
-) => {
- if (!userFqn) {
- return [];
- }
-
- const storageKey = `selectedColumns-${userFqn}`;
- const selectedColumns = JSON.parse(localStorage.getItem(storageKey) ?? '{}');
-
- if (entityType) {
- if (selectedColumns[entityType]) {
- return selectedColumns[entityType];
- } else if (!isFullViewTable) {
- localStorage.setItem(
- storageKey,
- JSON.stringify({
- ...selectedColumns,
- [entityType]: defaultColumns,
- })
- );
-
- return defaultColumns;
- }
- }
-
- return [];
-};
-
-export const handleUpdateTableColumnSelections = (
- selected: boolean,
- key: string,
- columnDropdownSelections: string[],
- userFqn: string,
- entityType: string | undefined
-) => {
- const updatedSelections = selected
- ? [...columnDropdownSelections, key]
- : columnDropdownSelections.filter((item) => item !== key);
-
- // Updating localStorage
- const selectedColumns = JSON.parse(
- localStorage.getItem(`selectedColumns-${userFqn}`) ?? '{}'
- );
- if (entityType) {
- localStorage.setItem(
- `selectedColumns-${userFqn}`,
- JSON.stringify({
- ...selectedColumns,
- [entityType]: updatedSelections,
- })
- );
- }
-
- return updatedSelections;
-};
-
export const getTableDetailPageBaseTabs = ({
queryCount,
isTourOpen,