From d3868cf79f3ac6651c70a3f05cb6222144943223 Mon Sep 17 00:00:00 2001 From: Harshit Shah Date: Thu, 15 Oct 2026 12:23:14 +0530 Subject: [PATCH 01/34] refactor(ui): delete the dead antd form-field factory and its antd-only types getField/generateFormFields, FilterPattern, SanitizedInput, UserTeamSelectableListSearchInput, AsyncSelect and SchedularUtils had no callers left in OSS. Also replace antd Rule/RuleObject/DefaultOptionType/Gutter types with local ones or drop the dead code that used them. --- .../DataInsight/PageViewsByEntitiesChart.tsx | 1 - .../TotalEntityInsightSummary.component.tsx | 2 - .../AddIngestion/AddIngestion.test.tsx | 5 - .../common/AsyncSelect/AsyncSelect.test.tsx | 325 -------------- .../common/AsyncSelect/AsyncSelect.tsx | 172 -------- .../AsyncSelect/AsyncSelectList.interface.ts | 23 - .../ManageButton/ManageButton.tsx | 9 +- .../FilterPattern/FilterPattern.test.tsx | 86 ---- .../common/FilterPattern/FilterPattern.tsx | 126 ------ .../FilterPattern/filterPattern.interface.ts | 27 -- .../SanitizedInput/SanitizedInput.test.tsx | 47 --- .../common/SanitizedInput/SanitizedInput.tsx | 31 -- ...eamSelectableListSearchInput.component.tsx | 175 -------- ...ser-team-selectable-list-search-input.less | 52 --- .../ui/src/constants/DataInsight.constants.ts | 1 - .../ui/src/constants/Form.constants.ts | 9 +- .../ui/src/interface/FormUtils.interface.ts | 51 --- .../src/pages/AppInstall/AppInstall.test.tsx | 4 - .../ui/src/utils/Alerts/AlertsUtil.test.tsx | 7 - .../ui/src/utils/Alerts/AlertsUtilPure.ts | 20 - .../CSV/EntityBulkEditConfigClassBase.ts | 14 +- .../ui/src/utils/FilterQueryUtils.test.ts | 67 --- .../ui/src/utils/FilterQueryUtils.ts | 15 +- .../ui/src/utils/SchedularUtils.test.tsx | 227 ---------- .../resources/ui/src/utils/SchedularUtils.tsx | 34 -- .../ui/src/utils/SearchSettingsUtils.ts | 16 - ...mUtils.test.tsx => formPureUtils.test.tsx} | 88 +--- .../main/resources/ui/src/utils/formUtils.tsx | 398 ------------------ 28 files changed, 16 insertions(+), 2016 deletions(-) delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelect/AsyncSelect.test.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelect/AsyncSelect.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelect/AsyncSelectList.interface.ts delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/FilterPattern/FilterPattern.test.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/FilterPattern/FilterPattern.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/FilterPattern/filterPattern.interface.ts delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/SanitizedInput/SanitizedInput.test.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/SanitizedInput/SanitizedInput.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/UserTeamSelectableListSearchInput/UserTeamSelectableListSearchInput.component.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/UserTeamSelectableListSearchInput/user-team-selectable-list-search-input.less delete mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/FilterQueryUtils.test.ts delete mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/SchedularUtils.test.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/SchedularUtils.tsx rename openmetadata-ui/src/main/resources/ui/src/utils/{formUtils.test.tsx => formPureUtils.test.tsx} (82%) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataInsight/PageViewsByEntitiesChart.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataInsight/PageViewsByEntitiesChart.tsx index 891889ccf4be..5f834b343dd3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataInsight/PageViewsByEntitiesChart.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataInsight/PageViewsByEntitiesChart.tsx @@ -179,7 +179,6 @@ const PageViewsByEntitiesChart: FC = ({ chartFilter, selectedDays }) => { allowFilter activeKeys={activeKeys} entities={sortedEntitiesByValue} - gutter={DI_STRUCTURE.rightRowGutter} latestData={latestData} relativePercentage={relativePercentage} selectedDays={selectedDays} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataInsight/TotalEntityInsightSummary.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataInsight/TotalEntityInsightSummary.component.tsx index 40d940e7de5b..b4e76e04168c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataInsight/TotalEntityInsightSummary.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataInsight/TotalEntityInsightSummary.component.tsx @@ -12,7 +12,6 @@ */ import { Button, Grid } from '@openmetadata/ui-core-components'; import { useChartPalette } from '@openmetadata/ui-core-components/charts'; -import { Gutter } from 'antd/lib/grid/row'; import classNames from 'classnames'; import { includes, startCase, toLower } from 'lodash'; import { useMemo, useState } from 'react'; @@ -30,7 +29,6 @@ type TotalEntityInsightSummaryProps = { selectedDays: number; entities: string[]; latestData: Record; - gutter?: Gutter | [Gutter, Gutter]; onActiveKeysUpdate?: (entity: string[]) => void; onActiveKeyMouseHover?: (entity: string) => void; activeKeys?: string[]; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/AddIngestion.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/AddIngestion.test.tsx index 64a88f0c0540..8fad68f8fe5c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/AddIngestion.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/AddIngestion.test.tsx @@ -91,11 +91,6 @@ jest.mock('../Ingestion/IngestionWorkflowForm/IngestionWorkflowForm', () => { )); }); -jest.mock('../../../../utils/SchedularUtils', () => ({ - getScheduleOptionsFromSchedules: jest.fn().mockReturnValue([]), - getRaiseOnErrorFormField: jest.fn().mockReturnValue({}), -})); - jest.mock('../../../../hooks/useEntityRules', () => ({ useEntityRules: jest.fn().mockReturnValue({ entityRules: { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelect/AsyncSelect.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelect/AsyncSelect.test.tsx deleted file mode 100644 index b0f0dd567eb1..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelect/AsyncSelect.test.tsx +++ /dev/null @@ -1,325 +0,0 @@ -/* - * Copyright 2025 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 { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { DefaultOptionType } from 'antd/lib/select'; -import { PagingResponse } from '../../../interface/common/paging.interface'; -import { showErrorToast } from '../../../utils/ToastUtils'; -import { AsyncSelect } from './AsyncSelect'; - -// Mock dependencies -jest.mock('../../../utils/ToastUtils', () => ({ - showErrorToast: jest.fn(), -})); - -jest.mock('../Loader/Loader', () => { - return function MockLoader({ size }: { size?: string }) { - return
Loading...
; - }; -}); - -// Mock debounce to make tests synchronous -jest.mock('lodash', () => ({ - ...jest.requireActual('lodash'), - debounce: (fn: unknown) => fn, -})); - -const mockOptions: DefaultOptionType[] = [ - { label: 'Option 1', value: 'option1' }, - { label: 'Option 2', value: 'option2' }, - { label: 'Option 3', value: 'option3' }, -]; - -const mockPaginatedResponse: PagingResponse = { - data: mockOptions, - paging: { - total: 10, - after: 'cursor1', - before: 'cursor2', - }, -}; - -describe('AsyncSelect Component', () => { - const mockApi = jest.fn(); - const mockOnSelect = jest.fn(); - - beforeEach(() => { - jest.clearAllMocks(); - (showErrorToast as jest.Mock).mockClear(); - }); - - describe('Basic Functionality', () => { - it('should render AsyncSelect component', () => { - mockApi.mockResolvedValue(mockOptions); - - render( - - ); - - expect(screen.getByTestId('async-select')).toBeInTheDocument(); - }); - - it('should call API on mount with empty search', async () => { - mockApi.mockResolvedValue(mockOptions); - - render(); - - await waitFor(() => { - expect(mockApi).toHaveBeenCalledWith('', undefined); - }); - }); - - it('should display loading state initially', async () => { - mockApi.mockImplementation( - () => - new Promise((resolve) => setTimeout(() => resolve(mockOptions), 100)) - ); - - render(); - - expect(screen.getByTestId('loader-small')).toBeInTheDocument(); - }); - - it('should populate options after API call', async () => { - mockApi.mockResolvedValue(mockOptions); - - render(); - - fireEvent.mouseDown(screen.getByRole('combobox')); - - await waitFor(() => { - expect(screen.getByText('Option 1')).toBeInTheDocument(); - expect(screen.getByText('Option 2')).toBeInTheDocument(); - expect(screen.getByText('Option 3')).toBeInTheDocument(); - }); - }); - - it('should call API with search text when typing', async () => { - mockApi.mockResolvedValue(mockOptions); - - render(); - - const input = screen.getByRole('combobox'); - fireEvent.change(input, { target: { value: 'test search' } }); - - await waitFor(() => { - expect(mockApi).toHaveBeenCalledWith('test search', undefined); - }); - }); - - it('should handle option selection', async () => { - mockApi.mockResolvedValue(mockOptions); - - render( - - ); - - fireEvent.mouseDown(screen.getByRole('combobox')); - - await waitFor(() => { - expect(screen.getByText('Option 1')).toBeInTheDocument(); - }); - - fireEvent.click(screen.getByText('Option 1')); - - expect(mockOnSelect).toHaveBeenCalled(); - }); - - it('should handle API errors gracefully', async () => { - const error = new Error('API Error'); - mockApi.mockRejectedValue(error); - - render(); - - await waitFor(() => { - expect(showErrorToast).toHaveBeenCalledWith(error); - }); - }); - }); - - describe('Infinite Scroll Functionality', () => { - it('should enable infinite scroll when enableInfiniteScroll is true', async () => { - mockApi.mockResolvedValue(mockPaginatedResponse); - - render( - - ); - - await waitFor(() => { - expect(mockApi).toHaveBeenCalledWith('', 1); - }); - }); - - it('should handle paginated response correctly', async () => { - mockApi.mockResolvedValue(mockPaginatedResponse); - - render( - - ); - - fireEvent.mouseDown(screen.getByRole('combobox')); - - await waitFor(() => { - expect(screen.getByText('Option 1')).toBeInTheDocument(); - expect(screen.getByText('Option 2')).toBeInTheDocument(); - expect(screen.getByText('Option 3')).toBeInTheDocument(); - }); - }); - - it('should not load more when all options are loaded', async () => { - const limitedResponse: PagingResponse = { - data: mockOptions, - paging: { total: 3 }, // Same as data length - }; - - mockApi.mockResolvedValue(limitedResponse); - - render( - - ); - - fireEvent.mouseDown(screen.getByRole('combobox')); - - await waitFor(() => { - expect(screen.getByText('Option 1')).toBeInTheDocument(); - }); - - // Simulate scroll to bottom - const dropdown = screen.getByRole('listbox'); - Object.defineProperty(dropdown, 'scrollTop', { - value: 100, - writable: true, - }); - Object.defineProperty(dropdown, 'offsetHeight', { - value: 100, - writable: true, - }); - Object.defineProperty(dropdown, 'scrollHeight', { - value: 200, - writable: true, - }); - - fireEvent.scroll(dropdown); - - // Should not call API again since all options are loaded - expect(mockApi).toHaveBeenCalledTimes(1); - }); - - it('should reset page when search text changes', async () => { - mockApi.mockResolvedValue(mockPaginatedResponse); - - render( - - ); - - const input = screen.getByRole('combobox'); - - // Initial load - await waitFor(() => { - expect(mockApi).toHaveBeenCalledWith('', 1); - }); - - // Change search text - fireEvent.change(input, { target: { value: 'new search' } }); - - await waitFor(() => { - expect(mockApi).toHaveBeenCalledWith('new search', 1); - }); - }); - }); - - describe('Configuration Options', () => { - it('should use custom debounce timeout', async () => { - mockApi.mockResolvedValue(mockOptions); - - render( - - ); - - // Since we mocked debounce to be synchronous, this just ensures the prop is passed - expect(screen.getByTestId('async-select')).toBeInTheDocument(); - }); - - it('should work without infinite scroll by default', async () => { - mockApi.mockResolvedValue(mockOptions); - - render(); - - await waitFor(() => { - expect(mockApi).toHaveBeenCalledWith('', undefined); - }); - }); - }); - - describe('Edge Cases', () => { - it('should handle empty API response', async () => { - mockApi.mockResolvedValue([]); - - render(); - - fireEvent.mouseDown(screen.getByRole('combobox')); - - await waitFor(() => { - expect(mockApi).toHaveBeenCalled(); - }); - - // Should not crash and should show no options - expect(screen.queryByText('Option 1')).not.toBeInTheDocument(); - }); - - it('should handle scroll events when infinite scroll is disabled', async () => { - mockApi.mockResolvedValue(mockOptions); - - render(); - - fireEvent.mouseDown(screen.getByRole('combobox')); - - await waitFor(() => { - expect(screen.getByText('Option 1')).toBeInTheDocument(); - }); - - // Simulate scroll - should not trigger additional API calls - const dropdown = screen.getByRole('listbox'); - fireEvent.scroll(dropdown); - - expect(mockApi).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelect/AsyncSelect.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelect/AsyncSelect.tsx deleted file mode 100644 index 891e5c974f8a..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelect/AsyncSelect.tsx +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright 2022 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 { Select, SelectProps } from 'antd'; -import { DefaultOptionType } from 'antd/lib/select'; -import { AxiosError } from 'axios'; -import { debounce, isObject } from 'lodash'; -import React, { useCallback, useEffect, useState } from 'react'; -import { Paging } from '../../../generated/type/paging'; -import { PagingResponse } from '../../../interface/common/paging.interface'; -import { showErrorToast } from '../../../utils/ToastUtils'; -import Loader from '../Loader/Loader'; -import { AsyncSelectListProps } from './AsyncSelectList.interface'; - -// Interface for paginated API response - -/** - * AsyncSelect to work with options provided from API directly - * Pass api reference or a function which can communicate with API and return with DefaultOptionType[] - * Additional configuration can be provided once needed like: debounce value, defaultOptions etc - * @param param0 - * @returns ReactComponent with select functionality - */ -export const AsyncSelect = ({ - options, - api, - enableInfiniteScroll = false, - debounceTimeout = 400, - ...restProps -}: SelectProps & AsyncSelectListProps) => { - const [optionsInternal, setOptionsInternal] = useState(); - const [loadingOptions, setLoadingOptions] = useState(false); - const [hasContentLoading, setHasContentLoading] = useState(false); - const [searchText, setSearchText] = useState(''); - const [paging, setPaging] = useState({} as Paging); - const [currentPage, setCurrentPage] = useState(1); - - useEffect(() => { - setOptionsInternal(options); - }, [options]); - - const fetchOptions = useCallback( - debounce(async (value: string, page = 1) => { - if (page === 1) { - setLoadingOptions(true); - setOptionsInternal([]); - } else { - setHasContentLoading(true); - } - - try { - const response = await api( - value, - enableInfiniteScroll ? page : undefined - ); - - if ( - enableInfiniteScroll && - response && - isObject(response) && - 'data' in response - ) { - // Handle paginated response - const pagingResponse = response as PagingResponse< - DefaultOptionType[] - >; - if (page === 1) { - setOptionsInternal(pagingResponse.data); - setPaging(pagingResponse.paging); - setCurrentPage(1); - } else { - setOptionsInternal((prev) => [ - ...(prev || []), - ...pagingResponse.data, - ]); - setPaging(pagingResponse.paging); - setCurrentPage(page); - } - } else { - // Handle simple array response - const simpleResponse = response as DefaultOptionType[]; - setOptionsInternal(simpleResponse); - } - } catch (error) { - showErrorToast(error as AxiosError); - } finally { - setLoadingOptions(false); - setHasContentLoading(false); - } - }, debounceTimeout), - [api, enableInfiniteScroll, debounceTimeout] - ); - - const handleSelection = useCallback(() => { - setSearchText(''); - }, []); - - const onScroll = useCallback( - async (e: React.UIEvent) => { - if (!enableInfiniteScroll) { - return; - } - - const { currentTarget } = e; - if ( - currentTarget.scrollTop + currentTarget.offsetHeight === - currentTarget.scrollHeight - ) { - const currentOptionsLength = optionsInternal?.length ?? 0; - if (currentOptionsLength < paging.total && !hasContentLoading) { - await fetchOptions(searchText, currentPage + 1); - } - } - }, - [ - enableInfiniteScroll, - optionsInternal, - paging.total, - hasContentLoading, - searchText, - currentPage, - fetchOptions, - ] - ); - - const dropdownRender = useCallback( - (menu: React.ReactElement) => ( - <> - {menu} - {hasContentLoading && } - - ), - [hasContentLoading] - ); - - useEffect(() => { - if (!restProps.disabled) { - fetchOptions(searchText, 1); - } - }, [searchText, restProps.disabled]); - - return ( - getIncludeValue(value, type)} - /> - - {includePatternExtraInfo && ( - - {includePatternExtraInfo} - - )} - - - - {t('label.exclude')}: - - ; -}; - -export default memo(SanitizedInput); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/UserTeamSelectableListSearchInput/UserTeamSelectableListSearchInput.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/UserTeamSelectableListSearchInput/UserTeamSelectableListSearchInput.component.tsx deleted file mode 100644 index b77a32bc4a34..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/UserTeamSelectableListSearchInput/UserTeamSelectableListSearchInput.component.tsx +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright 2025 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 { Select } from 'antd'; -import { noop } from 'lodash'; -import type { CustomTagProps } from 'rc-select/lib/BaseSelect'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { EntityReference } from '../../../generated/entity/teams/user'; -import { getEntityName } from '../../../utils/EntityNameUtils'; -import { UserTag } from '../UserTag/UserTag.component'; -import { UserTagSize } from '../UserTag/UserTag.interface'; -import { UserTeamSelectableList } from '../UserTeamSelectableList/UserTeamSelectableList.component'; -import { UserSelectDropdownProps } from '../UserTeamSelectableList/UserTeamSelectableList.interface'; -import './user-team-selectable-list-search-input.less'; - -interface UserTeamSelectableListSearchProps extends UserSelectDropdownProps { - disabled?: boolean; - placeholder?: string; -} - -const UserTeamSelectableListSearchInput: React.FC< - UserTeamSelectableListSearchProps -> = ({ - disabled, - hasPermission, - owner, - onUpdate = noop, - onClose, - multiple, - label, - previewSelected = false, - listHeight, - tooltipText, - placeholder, -}) => { - const [popoverVisible, setPopoverVisible] = useState(false); - const [selectedUsers, setSelectedUsers] = useState([]); - - const handleFocus = useCallback(() => { - setPopoverVisible(true); - }, []); - - const handleClose = () => { - setPopoverVisible(false); - if (onClose) { - onClose(); - } - }; - - const handlePopoverVisibleChange = (visible: boolean) => { - setPopoverVisible(visible); - if (!visible && onClose) { - onClose(); - } - }; - - const handleUpdate = async (updatedUser?: EntityReference[]) => { - if (onUpdate) { - setSelectedUsers(updatedUser ?? []); - handleClose(); - onUpdate(updatedUser); - } - }; - - const handleOnChangeSelect = (value: string[]) => { - if (onUpdate) { - const updatedUser = selectedUsers.filter((item) => - value.includes(item.name ?? '') - ); - - setSelectedUsers(updatedUser ?? []); - handleClose(); - onUpdate(updatedUser); - } - }; - - const selectedValues = useMemo( - () => - selectedUsers - .map((user) => user.name) - .filter((name): name is string => Boolean(name)), - [selectedUsers] - ); - - const customTagRender = (props: CustomTagProps) => { - const { value, closable, onClose } = props; - - const selectedAssignee = selectedUsers?.find( - (option) => option.name === value - ); - - const tagProps = { - id: selectedAssignee?.name ?? value, - name: getEntityName(selectedAssignee) || value, - closable: closable, - onRemove: onClose, - size: UserTagSize.small, - isTeam: selectedAssignee?.type === 'team', - className: 'assignee-tag', - }; - - return ; - }; - - const selectInput = useMemo(() => { - return ( - , - }), - [FieldTypes.SLIDER_INPUT]: ({ props }) => ({ - element: ( - - ), - }), - [FieldTypes.DESCRIPTION]: ({ props }) => ({ - element: , - formPropsPatch: { - trigger: 'onTextChange', - initialValue: props?.initialValue ?? '', - }, - }), - [FieldTypes.TAG_SUGGESTION]: ({ props }) => ({ - element: ( - - ), - }), - [FieldTypes.ASYNC_SELECT_LIST]: ({ props }) => ({ - element: ( - - ), - }), - [FieldTypes.DOMAIN_SELECT]: ({ props }) => { - const { children, ...rest } = props; - - return { - element: ( - - {children} - - ), - }; - }, - [FieldTypes.USER_TEAM_SELECT]: ({ props }) => { - const { children, ...rest } = props; - - return { - element: ( - - {children} - - ), - }; - }, - [FieldTypes.USER_TEAM_SELECT_INPUT]: ({ props }) => ({ - element: ( - - ), - }), - [FieldTypes.USER_MULTI_SELECT]: ({ props }) => { - const { children, ...rest } = props; - - return { - element: ( - - {children} - - ), - }; - }, - [FieldTypes.COMPONENT]: ({ props }) => ({ - element: props.children, - }), -}; - -const renderUtTextField = ({ - formProps, - props, - id, - placeholder, - label, - fieldRules, -}: FieldRenderContext) => { - const isRequired = (fieldRules ?? []).some( - (rule) => (rule as RuleObject).required - ); - const { 'data-testid': dataTestId, ...inputRest } = props; - - return ( - ({ value: (value as string) ?? '' })}> - >)} - id={id} - inputDataTestId={dataTestId as string} - isRequired={isRequired} - label={isString(label) ? label : undefined} - placeholder={placeholder} - /> - - ); -}; - -const renderUtSelectField = ({ - formProps, - props, - id, - placeholder, - label, - fieldRules, -}: FieldRenderContext) => { - const isRequired = (fieldRules ?? []).some( - (rule) => (rule as RuleObject).required - ); - const { items = [], ...selectRest } = props as { - items?: SelectItemType[]; - } & Record; - - return ( - ({ selectedKey: value ?? null })} - trigger="onSelectionChange" - validateTrigger="onSelectionChange" - valuePropName="selectedKey"> - >)} - id={id} - isRequired={isRequired} - items={items} - label={isString(label) ? label : undefined} - placeholder={placeholder}> - {(item: SelectItemType) => } - - - ); -}; - -const renderUtSwitchField = ({ - formProps, - props, - label, -}: FieldRenderContext) => { - const { isDisabled, onChange, size, ...switchRest } = props as ToggleProps; - - return ( - - - - ); -}; - -// These field types render their own complete Form.Item (a different shape -// than the common wrapper below) and return immediately, matching the -// original switch-case early returns. -const CUSTOM_FIELD_RENDERERS: Partial< - Record ReactNode> -> = { - [FieldTypes.UT_TEXT]: renderUtTextField, - [FieldTypes.UT_SELECT]: renderUtSelectField, - [FieldTypes.UT_SWITCH]: renderUtSwitchField, -}; - -const shouldShowHelperAlert = ( - helperTextType: HelperTextType | undefined, - helperText: ReactNode, - showHelperText: boolean -): boolean => - helperTextType === HelperTextType.ALERT && - Boolean(helperText) && - showHelperText; - -export const getField = (field: FieldProp) => { - const { - label, - name, - type, - helperText, - helperTextType, - showHelperText = true, - required, - props = {}, - rules = [], - placeholder, - id, - formItemProps, - hasSeparator = false, - formItemLayout = FormItemLayout.VERTICAL, - isBeta = false, - newLook = false, - } = field; - - let fieldElement: ReactNode = null; - let fieldRules = [...rules]; - // Check if required rule is already present to avoid rule duplication - const isRequiredRulePresent = rules.some( - (rule) => (rule as RuleObject).required ?? false - ); - - if (required && !isRequiredRulePresent) { - fieldRules = [ - ...fieldRules, - { - required, - message: t('label.field-required', { - field: startCase(toString(name)), - }), - }, - ]; - } - - const formProps: FormItemProps = { - id: id, - name: name, - rules: fieldRules, - ...formItemProps, - }; - - const renderContext: FieldRenderContext = { - props, - id, - placeholder, - label, - formProps, - fieldRules, - }; - - const customRenderer = CUSTOM_FIELD_RENDERERS[type]; - if (customRenderer) { - return customRenderer(renderContext); - } - - const elementRenderer = FIELD_ELEMENT_RENDERERS[type]; - if (elementRenderer) { - const { element, formPropsPatch } = elementRenderer(renderContext); - fieldElement = element; - Object.assign(formProps, formPropsPatch); - } - - const labelValue = ( - - ); - - if (type === FieldTypes.SWITCH && newLook) { - return ( -
- - - - {labelValue} -
- ); - } - - return ( - - - {fieldElement} - - - {shouldShowHelperAlert(helperTextType, helperText, showHelperText) && ( - - )} - - {hasSeparator && } - - ); -}; - -export const generateFormFields = (fields: FieldProp[]) => { - return ( - <> - {fields.map((field, index) => ( - {getField(field)} - ))} - - ); -}; - export const setInlineErrorValue = ( description: string, serverAPIError: string, From d1de267a5fea6014387ad3ec9ac6ecdcf5a2dc58 Mon Sep 17 00:00:00 2001 From: Harshit Shah Date: Thu, 15 Oct 2026 12:33:02 +0530 Subject: [PATCH 02/34] refactor(ui): move the remaining antd date pickers to core Audit log export, table queries, pipeline executions, lineage time filter and the KPI form now use the core DateRangePicker/DatePicker (range filters through the existing DqDateRangeFilter adapter), with a core clear button where antd offered allowClear. Deletes the antd generatePicker wrapper and its luxon-only helpers, and drives the pickers through their roles in Playwright. --- .../ui/playwright/e2e/Pages/AuditLogs.spec.ts | 62 +---------- .../ui/playwright/utils/auditLogs.ts | 10 ++ .../ui/playwright/utils/dataInsight.ts | 17 +-- .../Database/TableQueries/TableQueries.tsx | 72 +++++-------- .../LineageTimeFilter.component.tsx | 100 ++++++++---------- .../Execution/Execution.component.tsx | 72 ++++++------- .../Pipeline/Execution/execution.less | 28 ----- .../common/DatePicker/DatePicker.tsx | 25 ----- .../ui/src/constants/DataInsight.constants.ts | 2 - .../AuditLogsPage/AuditLogsPage.test.tsx | 40 +++---- .../src/pages/AuditLogsPage/AuditLogsPage.tsx | 41 +++---- .../ui/src/pages/KPIPage/AddKPIPage.test.tsx | 5 - .../ui/src/pages/KPIPage/EditKPIPage.test.tsx | 9 -- .../ui/src/pages/KPIPage/KPIFormFields.tsx | 41 ++++--- .../ui/src/utils/DataInsightPureUtils.ts | 8 -- 15 files changed, 193 insertions(+), 339 deletions(-) delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Pipeline/Execution/execution.less delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/DatePicker/DatePicker.tsx diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/AuditLogs.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/AuditLogs.spec.ts index a64a9b60c297..7469d4cb5b02 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/AuditLogs.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/AuditLogs.spec.ts @@ -16,11 +16,11 @@ import { GlobalSettingOptions } from '../../constant/settings'; import { expect, test } from '../../support/fixtures/base'; import { navigateToAuditLogsPage, + selectTodayExportRange, verifyAuditEntryHasValidUUIDs, waitForAuditLogEntry, } from '../../utils/auditLogs'; import { getApiContext, redirectToHomePage } from '../../utils/common'; -import { customFormatDateTime } from '../../utils/dateTime'; import { settingClick } from '../../utils/sidebar'; test.use({ storageState: 'playwright/.auth/admin.json' }); @@ -718,25 +718,7 @@ test.describe( }); await test.step('Select date range', async () => { - const dateRangePicker = page.getByTestId('export-date-range-picker'); - await dateRangePicker.click(); - - await page.locator('.ant-picker-dropdown').waitFor({ - state: 'visible', - }); - - // RangePicker draws two month panels and each grid also renders the - // neighbouring month's days, all carrying a title. Near month end the - // next-month panel repeats today, so match only the cell that is - // in view for its own panel. - const todayCell = page.locator( - `.ant-picker-dropdown:visible .ant-picker-cell-in-view[title="${customFormatDateTime( - Date.now(), - 'yyyy-MM-dd' - )}"]` - ); - await todayCell.click(); - await todayCell.click(); + await selectTodayExportRange(page); }); await test.step('Verify Export button is enabled after date selection', async () => { @@ -818,25 +800,7 @@ test.describe( }); await test.step('Select date range and verify export includes search term', async () => { - const dateRangePicker = page.getByTestId('export-date-range-picker'); - await dateRangePicker.click(); - - await page.locator('.ant-picker-dropdown').waitFor({ - state: 'visible', - }); - - // RangePicker draws two month panels and each grid also renders the - // neighbouring month's days, all carrying a title. Near month end the - // next-month panel repeats today, so match only the cell that is - // in view for its own panel. - const todayCell = page.locator( - `.ant-picker-dropdown:visible .ant-picker-cell-in-view[title="${customFormatDateTime( - Date.now(), - 'yyyy-MM-dd' - )}"]` - ); - await todayCell.click(); - await todayCell.click(); + await selectTodayExportRange(page); const exportApiCall = page.waitForRequest( (request) => @@ -869,25 +833,7 @@ test.describe( state: 'visible', }); - const dateRangePicker = page.getByTestId('export-date-range-picker'); - await dateRangePicker.click(); - - await page.locator('.ant-picker-dropdown').waitFor({ - state: 'visible', - }); - - // RangePicker draws two month panels and each grid also renders the - // neighbouring month's days, all carrying a title. Near month end the - // next-month panel repeats today, so match only the cell that is - // in view for its own panel. - const todayCell = page.locator( - `.ant-picker-dropdown:visible .ant-picker-cell-in-view[title="${customFormatDateTime( - Date.now(), - 'yyyy-MM-dd' - )}"]` - ); - await todayCell.click(); - await todayCell.click(); + await selectTodayExportRange(page); }); await test.step('Trigger export and validate response', async () => { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/auditLogs.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/auditLogs.ts index 0abe1f3e74f2..fb6db61f0e93 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/auditLogs.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/auditLogs.ts @@ -16,6 +16,16 @@ import { settingClick } from './sidebar'; const AUDIT_LOG_POLL_TIMEOUT = 120000; +export const selectTodayExportRange = async (page: Page) => { + const modal = page.getByTestId('export-audit-logs-modal'); + await modal + .getByTestId('export-date-range-picker') + .getByRole('button', { name: 'Calendar Date range picker' }) + .click(); + await page.getByRole('button', { name: 'Today', exact: true }).click(); + await page.getByRole('button', { name: 'Apply' }).click(); +}; + export const navigateToAuditLogsPage = async (page: Page) => { const logRequest = page.waitForResponse('/api/v1/audit/logs?*'); await settingClick(page, GlobalSettingOptions.AUDIT_LOGS); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/dataInsight.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/dataInsight.ts index 3fee409cc308..0479257cae34 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/dataInsight.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/dataInsight.ts @@ -15,6 +15,7 @@ import { Kpi } from '../../src/generated/dataInsight/kpi/kpi'; import { KPIData } from '../constant/dataInsight.interface'; import { deleteFixtureEntity, okJson, settleAll } from './apiResponse'; import { fillDescriptionBox, selectOptionWithRetry } from './common'; +import { pickDateInCorePicker } from './dateTime'; export const deleteKpiRequest = async ( apiRequest: APIRequestContext, @@ -88,12 +89,16 @@ export const addKpi = async (page: Page, data: KPIData) => { ); await page.getByRole('spinbutton').fill('100'); - await page.getByTestId('start-date').click(); - await page.getByTestId('start-date').fill(startDate); - await page.getByTestId('start-date').press('Enter'); - await page.getByTestId('end-date').click(); - await page.getByTestId('end-date').fill(endDate); - await page.getByTestId('end-date').press('Enter'); + await pickDateInCorePicker( + page, + page.getByTestId('start-date').getByRole('button'), + startDate + ); + await pickDateInCorePicker( + page, + page.getByTestId('end-date').getByRole('button'), + endDate + ); await fillDescriptionBox(page, 'Playwright KPI test description'); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueries.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueries.tsx index ca12a234821f..874128dabc25 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueries.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueries.tsx @@ -12,17 +12,18 @@ */ import { - CloseCircleOutlined, SortAscendingOutlined, SortDescendingOutlined, } from '@ant-design/icons'; import { Box, Button, + ButtonUtility, Grid, Tooltip, Typography, } from '@openmetadata/ui-core-components'; +import { XClose } from '@openmetadata/ui-core-components/icons'; import { AxiosError } from 'axios'; import { compare } from 'fast-json-patch'; import { isEmpty, isUndefined, uniqBy } from 'lodash'; @@ -30,6 +31,7 @@ import Qs from 'qs'; import { FC, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; +import DqDateRangeFilter from '../../observability/DataQuality/Dashboard/DqDateRangeFilter'; import { WILD_CARD_CHAR } from '../../../constants/char.constants'; import { INITIAL_PAGING_VALUE, @@ -71,9 +73,6 @@ import { } from '../../../utils/Query/QueryUtils'; import { getAddQueryPath } from '../../../utils/RouterUtils'; import { showErrorToast } from '../../../utils/ToastUtils'; -import DatePicker, { - RangePickerProps, -} from '../../common/DatePicker/DatePicker'; import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; import FilterSelectDropdown from '../../common/FilterSelectDropdown/FilterSelectDropdown'; import Loader from '../../common/Loader/Loader'; @@ -118,7 +117,6 @@ const TableQueries: FC = ({ ); const [isTagsLoading, setIsTagsLoading] = useState(false); const [isOwnerLoading, setIsOwnerLoading] = useState(false); - const [isClickedCalendar, setIsClickedCalendar] = useState(false); const [queryDateFilter, setQueryDateFilter] = useState<{ startTs: number; endTs: number; @@ -431,23 +429,9 @@ const TableQueries: FC = ({ } }; - const onDateChange: RangePickerProps['onChange'] = (values) => { - if (values) { - const startTs = values[0]?.startOf('day').valueOf(); - const endTs = values[1]?.endOf('day').valueOf(); - - if (!isUndefined(startTs) && !isUndefined(endTs)) { - setQueryDateFilter({ startTs, endTs }); - fetchFilteredQueries({ - tags: tagsFilter.selected, - timeRange: { startTs, endTs }, - }); - setIsClickedCalendar(false); - } - } else { - setQueryDateFilter(undefined); - fetchFilteredQueries({ tags: tagsFilter.selected }); - } + const onDateChange = (timeRange?: { startTs: number; endTs: number }) => { + setQueryDateFilter(timeRange); + fetchFilteredQueries({ tags: tagsFilter.selected, timeRange }); }; const pagingHandler = (currentPage: number, pageSize: number) => { @@ -633,30 +617,28 @@ const TableQueries: FC = ({ onGetInitialOptions={getInitialTagsOptions} onSearch={handleTagsSearch} /> - { - setIsClickedCalendar(true); - }}> - - {t('label.created-date')} - } - data-testid="data-range-picker" - open={isClickedCalendar} - suffixIcon={null} - onChange={onDateChange} - onOpenChange={(isOpen) => { - setIsClickedCalendar(isOpen); - }} + + {t('label.created-date')} + + {queryDateFilter && ( + } + onClick={() => onDateChange()} /> - - + )} + [0]['value']; + const PRESET_DAYS: Record< Exclude< LineageTimePresetKey, @@ -49,6 +62,7 @@ const LineageTimeFilter: FC = ({ const { i18n, t } = useTranslation(); const [showCustomRange, setShowCustomRange] = useState(false); const [showPointInTime, setShowPointInTime] = useState(false); + const [pointValue, setPointValue] = useState(null); const matchedPresetDays = useMemo(() => { if (startTime === undefined || endTime === undefined) { @@ -112,6 +126,9 @@ const LineageTimeFilter: FC = ({ return; } if (key === LineageTimePresetKey.PointInTime) { + setPointValue( + (isPointInTime ? millisToDateValue(endTime) : null) as CoreDateValue + ); setShowPointInTime(true); return; @@ -124,41 +141,24 @@ const LineageTimeFilter: FC = ({ }); } }, - [onChange] + [onChange, isPointInTime, endTime] ); - const handleCustomChange = useCallback( - (values: [DateTime | null, DateTime | null] | null) => { - if (!values) { - onChange({ startTime: undefined, endTime: undefined }); - setShowCustomRange(false); - - return; - } - const start = values[0]?.startOf('day').toMillis(); - const end = values[1]?.endOf('day').toMillis(); - onChange({ startTime: start, endTime: end }); - setShowCustomRange(false); - }, + const handleCustomApply = useCallback( + ({ startTs, endTs }: { startTs: number; endTs: number }) => + onChange({ startTime: startTs, endTime: endTs }), [onChange] ); - const handlePointInTimeChange = useCallback( - (value: DateTime | null) => { - if (!value) { - onChange({ startTime: undefined, endTime: undefined }); - setShowPointInTime(false); - - return; - } + const handlePointInTimeApply = useCallback(() => { + if (pointValue) { + // The date-only value resolves to midnight; include the whole day. onChange({ startTime: undefined, - endTime: value.endOf('day').toMillis(), + endTime: dateValueToMillis(pointValue.add({ days: 1 })) - 1, }); - setShowPointInTime(false); - }, - [onChange] - ); + } + }, [onChange, pointValue]); const presetItems = useMemo( () => [ @@ -244,35 +244,25 @@ const LineageTimeFilter: FC = ({ {showCustomRange && ( - value.toFormat('yyyy-LL-dd')} - open={showCustomRange} - placement="bottomRight" - value={ - startTime !== undefined && endTime !== undefined - ? [DateTime.fromMillis(startTime), DateTime.fromMillis(endTime)] - : null - } - onChange={handleCustomChange} - onOpenChange={(open) => setShowCustomRange(open)} - /> + + + )} {showPointInTime && ( - value.toFormat('yyyy-LL-dd')} - open={showPointInTime} - placement="bottomRight" - value={ - isPointInTime && endTime !== undefined - ? DateTime.fromMillis(endTime) - : null - } - onChange={handlePointInTimeChange} - onOpenChange={(open) => setShowPointInTime(open)} + value={pointValue} + onApply={handlePointInTimeApply} + onChange={setPointValue} + onOpenChange={setShowPointInTime} /> )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/Execution/Execution.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/Execution/Execution.component.tsx index d39bbdf33b90..3ccc2e022160 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/Execution/Execution.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/Execution/Execution.component.tsx @@ -11,20 +11,20 @@ * limitations under the License. */ -import Icon, { CloseCircleOutlined } from '@ant-design/icons'; import { Box, Button, ButtonGroup, ButtonGroupItem, + ButtonUtility, Dropdown, Grid, } from '@openmetadata/ui-core-components'; +import { XClose } from '@openmetadata/ui-core-components/icons'; import { AxiosError } from 'axios'; -import { isNaN, map } from 'lodash'; +import { map } from 'lodash'; import { Key, useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { ReactComponent as Calendar } from '../../../assets/svg/calendar.svg'; import { ReactComponent as FilterIcon } from '../../../assets/svg/filter.svg'; import { EXECUTION_FILTER_RANGE, @@ -43,10 +43,7 @@ import { getEpochMillisForPastDays, } from '../../../utils/date-time/DateTimeUtils'; import { showErrorToast } from '../../../utils/ToastUtils'; -import DatePicker, { - RangePickerProps, -} from '../../common/DatePicker/DatePicker'; -import './execution.less'; +import DqDateRangeFilter from '../../observability/DataQuality/Dashboard/DqDateRangeFilter'; import ListView from './ListView/ListViewTab.component'; import TreeViewTab from './TreeView/TreeViewTab.component'; @@ -92,33 +89,13 @@ const ExecutionsTab = ({ pipelineFQN, tasks }: ExecutionProps) => { [] ); - const onDateChange: RangePickerProps['onChange'] = (values) => { - if (values) { - const startTime = values[0]?.startOf('day').valueOf() ?? 0; - const endTime = values[1]?.endOf('day').valueOf() ?? 0; - - if (!isNaN(startTime) && !isNaN(endTime)) { - setStartTime(startTime); - setEndTime(endTime); - } - if (isNaN(startTime)) { - setIsClickedCalendar(false); - setStartTime( - getEpochMillisForPastDays(EXECUTION_FILTER_RANGE.last365days.days) - ); - setEndTime(getCurrentMillis()); - - setDatesSelected(false); - } - - setDatesSelected(true); - } else { - setDatesSelected(false); - setStartTime( + const onDateChange = (range?: { startTs: number; endTs: number }) => { + setDatesSelected(Boolean(range)); + setStartTime( + range?.startTs ?? getEpochMillisForPastDays(EXECUTION_FILTER_RANGE.last365days.days) - ); - setEndTime(getCurrentMillis()); - } + ); + setEndTime(range?.endTs ?? getCurrentMillis()); }; useEffect(() => { @@ -194,16 +171,27 @@ const ExecutionsTab = ({ pipelineFQN, tasks }: ExecutionProps) => { {view === PIPELINE_EXECUTION_TABS.LIST_VIEW ? ( - } + } - onChange={onDateChange} - /> + gap={2}> + + {datesSelected && ( + } + onClick={() => onDateChange()} + /> + )} + ) : null} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/Execution/execution.less b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/Execution/execution.less deleted file mode 100644 index ee60625ba836..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/Execution/execution.less +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2022 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. - */ - -.ant-picker-range.executions-date-picker { - padding: 0 var(--om-space-6) 0 0; - max-width: 200px; - .ant-picker-range-separator { - display: none; - } - .ant-picker-clear { - right: -6px; - } - - .ant-picker-active-bar { - margin-left: 0; - width: 85px !important; - } -} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DatePicker/DatePicker.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DatePicker/DatePicker.tsx deleted file mode 100644 index f397832fabba..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DatePicker/DatePicker.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2025 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 generatePicker, { - RangePickerBaseProps as BaseRangePickerProps, -} from 'antd/lib/date-picker/generatePicker'; -import { DateTime } from 'luxon'; -import luxonGenerateConfig from 'rc-picker/lib/generate/luxon'; - -const DatePicker = generatePicker(luxonGenerateConfig); - -type RangePickerProps = BaseRangePickerProps; - -export default DatePicker; - -export type { RangePickerProps }; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/DataInsight.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/DataInsight.constants.ts index aeb4ee884167..f9e7cc994278 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/DataInsight.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/DataInsight.constants.ts @@ -121,8 +121,6 @@ export const ENTITIES_SUMMARY_LIST: Array = [ }, ]; -export const KPI_DATE_PICKER_FORMAT = 'yyyy-MM-dd'; - export const BASE_COLORS = [ '#E57373', '#BA68C8', diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AuditLogsPage/AuditLogsPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AuditLogsPage/AuditLogsPage.test.tsx index 33b235397ef7..21ccd31f09b4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AuditLogsPage/AuditLogsPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AuditLogsPage/AuditLogsPage.test.tsx @@ -72,6 +72,23 @@ jest.mock('@openmetadata/ui-core-components', () => ({ .mockImplementation(({ children, 'data-testid': testId }) => (
{children}
)), + DateRangePicker: jest + .fn() + .mockImplementation(({ onChange, 'data-testid': testId }) => { + const day = { + toDate: () => new Date(0), + add: () => ({ toDate: () => new Date(86_400_000) }), + }; + + return ( + + ); + }), Input: jest .fn() .mockImplementation(({ value, onChange, inputDataTestId, placeholder }) => ( @@ -147,29 +164,6 @@ jest.mock('../../rest/auditLogAPI', () => ({ // The real range picker cannot be driven from jsdom, so expose a button that // hands the page a valid range and lets the export flow run end to end. -const mockRangeDay = { - startOf: () => ({ valueOf: () => 1 }), - endOf: () => ({ valueOf: () => 2 }), -}; - -jest.mock('../../components/common/DatePicker/DatePicker', () => ({ - __esModule: true, - default: { - RangePicker: ({ - onChange, - }: { - onChange: (dates: [unknown, unknown]) => void; - }) => ( - - ), - }, -})); - jest.mock('../../components/PageHeader/PageHeader.component', () => jest.fn().mockReturnValue(
Page Header
) ); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AuditLogsPage/AuditLogsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AuditLogsPage/AuditLogsPage.tsx index 3e8673a6d6b2..e6e1b9794fcb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AuditLogsPage/AuditLogsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AuditLogsPage/AuditLogsPage.tsx @@ -16,6 +16,7 @@ import { Button, ButtonUtility, Card, + DateRangePicker, Input, ProgressBar, SimpleModal, @@ -32,8 +33,6 @@ import AuditLogFilters from '../../components/AuditLog/AuditLogFilters.component import AuditLogList from '../../components/AuditLog/AuditLogList.component'; import '../../components/common/atoms/filters/FilterSelection.less'; import Banner from '../../components/common/Banner/Banner'; -import DatePicker from '../../components/common/DatePicker/DatePicker'; -import { getPopupContainer } from '../../components/common/DrawerPopupContainerProvider/DrawerPopupContainerProvider'; import HeaderBreadcrumb from '../../components/common/HeaderBreadcrumb/HeaderBreadcrumb.component'; import NextPrevious from '../../components/common/NextPrevious/NextPrevious'; import { PagingHandlerParams } from '../../components/common/NextPrevious/NextPrevious.interface'; @@ -65,10 +64,18 @@ import { } from '../../types/auditLogs.interface'; import { buildParamsFromFilters } from '../../utils/AuditLogUtils'; import { CUSTOM_DATE_RANGE_KEY } from '../../utils/DatePickerMenuUtils'; +import { + dateValueToMillis, + millisToDateValue, +} from '../../utils/date-time/calendarDate.utils'; import { getSettingPath } from '../../utils/RouterUtils'; import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; import './AuditLogsPage.less'; +// Core resolves its own @internationalized/date copy, so its DateValue is a +// distinct nominal type from the app's; type picker state with core's own. +type CorePickerProps = Parameters[0]; + // Fallback cadence for the export modal when the completion websocket event is // delivered to a different server than the one holding this client's socket. const EXPORT_POLL_INTERVAL_MS = 5000; @@ -107,9 +114,8 @@ const AuditLogsPage = () => { const [pageSize, setPageSize] = useState(PAGE_SIZE_MEDIUM); const [isExportModalOpen, setIsExportModalOpen] = useState(false); - const [exportDateRange, setExportDateRange] = useState< - [DateTime, DateTime] | null - >(null); + const [exportDateRange, setExportDateRange] = + useState(null); const [isExporting, setIsExporting] = useState(false); const [exportJob, setExportJob] = useState(null); const exportJobRef = useRef(null); @@ -386,8 +392,9 @@ const AuditLogsPage = () => { setIsExporting(true); try { const response = await exportAuditLogs({ - startTs: exportDateRange[0].startOf('day').valueOf(), - endTs: exportDateRange[1].endOf('day').valueOf(), + startTs: dateValueToMillis(exportDateRange.start), + // The date-only end value resolves to midnight; include the whole day. + endTs: dateValueToMillis(exportDateRange.end.add({ days: 1 })) - 1, q: searchTerm || undefined, ...filterParams, }); @@ -624,21 +631,15 @@ const AuditLogsPage = () => { size="text-md"> {t('label.date-range')} * - current > DateTime.now().endOf('day')} - getPopupContainer={getPopupContainer} + isDisabled={isExporting} + maxValue={ + millisToDateValue(Date.now()) as CorePickerProps['maxValue'] + } value={exportDateRange} - onChange={(dates) => { - if (dates?.[0] && dates?.[1]) { - setExportDateRange([dates[0], dates[1]]); - } else { - setExportDateRange(null); - } - }} + onChange={setExportDateRange} /> {renderExportProgress()} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/AddKPIPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/AddKPIPage.test.tsx index ed770f3f27ff..d9ed2d82da99 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/AddKPIPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/AddKPIPage.test.tsx @@ -59,11 +59,6 @@ jest.mock('../../components/common/ResizablePanels/ResizablePanels', () => jest.mock('../../utils/DataInsightPureUtils', () => ({ ...jest.requireActual('../../utils/DataInsightPureUtils'), getKpiTargetValueByMetricType: jest.fn().mockReturnValue(10), - getDisabledDates: jest.fn().mockReturnValue(true), -})); - -jest.mock('../../constants/DataInsight.constants', () => ({ - KPI_DATE_PICKER_FORMAT: 'YYY-MM-DD', })); const mockProps = { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/EditKPIPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/EditKPIPage.test.tsx index 713620e37ce1..109cf6ae454d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/EditKPIPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/EditKPIPage.test.tsx @@ -50,7 +50,6 @@ jest.mock('../../hooks/authHooks', () => ({ jest.mock('../../utils/DataInsightPureUtils', () => ({ ...jest.requireActual('../../utils/DataInsightUtils'), getKpiTargetValueByMetricType: jest.fn().mockReturnValue(10), - getDisabledDates: jest.fn().mockReturnValue(true), getDataInsightPathWithFqn: jest.fn().mockReturnValue(''), })); @@ -63,14 +62,6 @@ jest.mock('../../components/common/ResizablePanels/ResizablePanels', () => )) ); -jest.mock('../../constants/DataInsight.constants', () => ({ - KPI_DATE_PICKER_FORMAT: 'YYY-MM-DD', -})); - -jest.mock('../../components/common/DatePicker/DatePicker', () => - jest.fn().mockImplementation((props) => ) -); - const mockProps = { pageTitle: 'edit-kpi', }; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/KPIFormFields.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/KPIFormFields.tsx index 2e83d3c0a513..568ba15dd964 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/KPIFormFields.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/KPIFormFields.tsx @@ -13,6 +13,7 @@ import { Box, + DatePicker, FormField, FormItemLabel, Grid, @@ -24,13 +25,24 @@ import { import { DateTime } from 'luxon'; import { UseFormReturn } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import DatePicker from '../../components/common/DatePicker/DatePicker'; -import { KPI_DATE_PICKER_FORMAT } from '../../constants/DataInsight.constants'; import { KpiTargetType } from '../../generated/dataInsight/kpi/kpi'; -import { getDisabledDates } from '../../utils/DataInsightPureUtils'; +import { + dateValueToMillis, + millisToDateValue, +} from '../../utils/date-time/calendarDate.utils'; import { KPIMetricTypeOptions } from '../../utils/KPI/KPIUtils'; import { KPIFormValues } from './KPIPage.interface'; +// Core resolves its own @internationalized/date copy, so its DateValue is a +// distinct nominal type from the app's; convert at the picker boundary. +type CoreDateValue = Parameters[0]['value']; + +const toPickerValue = (value: DateTime | null) => + millisToDateValue(value?.toMillis()) as CoreDateValue; + +const fromPickerValue = (value: CoreDateValue) => + value ? DateTime.fromMillis(dateValueToMillis(value)) : null; + interface KPIFormFieldsProps { form: UseFormReturn; chartItems: Array<{ id: string; label: string }>; @@ -48,6 +60,7 @@ const KPIFormFields = ({ form, chartItems, isEditing }: KPIFormFieldsProps) => { const { t } = useTranslation(); const metricType = form.watch('metricType'); const startLabel = t('label.start-entity', { entity: t('label.date') }); + const todayValue = toPickerValue(DateTime.now()); // A start date counts from the start of its day; an end date before it moves the start back. const handleStartDate = (value: DateTime | null) => { @@ -205,13 +218,14 @@ const KPIFormFields = ({ form, chartItems, isEditing }: KPIFormFieldsProps) => { handleStartDate(fromPickerValue(value))} /> {fieldState.error && ( {fieldState.error.message} @@ -233,13 +247,14 @@ const KPIFormFields = ({ form, chartItems, isEditing }: KPIFormFieldsProps) => { handleEndDate(fromPickerValue(value))} /> {fieldState.error && ( {fieldState.error.message} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DataInsightPureUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DataInsightPureUtils.ts index b8c45400e92a..63f77655e8a9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DataInsightPureUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DataInsightPureUtils.ts @@ -19,8 +19,6 @@ import { round, toNumber, } from 'lodash'; -import { DateTime } from 'luxon'; -import type { RangePickerProps } from '../components/common/DatePicker/DatePicker'; import { PLACEHOLDER_ROUTE_TAB, ROUTES } from '../constants/constants'; import { SystemChartType } from '../enums/DataInsight.enum'; import { @@ -181,12 +179,6 @@ export const getFormattedActiveUsersData = ( }; }; -export const getDisabledDates: RangePickerProps['disabledDate'] = (current) => { - const today = DateTime.now().startOf('day'); - - return current ? current.startOf('day') < today : false; -}; - export const getDataInsightPathWithFqn = (tab = DataInsightTabs.DATA_ASSETS) => ROUTES.DATA_INSIGHT_WITH_TAB.replace(PLACEHOLDER_ROUTE_TAB, tab); From eb8a46cb1affa546e4ec0e280e1d08542ee3788f Mon Sep 17 00:00:00 2001 From: Harshit Shah Date: Thu, 15 Oct 2026 12:39:44 +0530 Subject: [PATCH 03/34] refactor(ui): move the CSV grid's antd selects to core Enum, tier and entity-type cells use a core Select that opens with the cell; custom-property enums and metric related-metrics/data-products use core Autocomplete (the latter in free-text mode, replacing antd mode="tags"). Drops the antd select styles and the grid's .ant-select key-handling checks. --- .../ui/playwright/utils/importUtils.ts | 4 +- .../ui/src/hooks/useGridEditController.ts | 4 +- .../bulk-entity-import-page.less | 109 ----------- .../src/utils/CSV/CSVUtilsClassBase.test.tsx | 40 +++- .../ui/src/utils/CSV/CSVUtilsClassBase.tsx | 181 +++++++++--------- 5 files changed, 129 insertions(+), 209 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/importUtils.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/importUtils.ts index 6f4ab5531d8c..bbfe842ec1a7 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/importUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/importUtils.ts @@ -576,8 +576,8 @@ export const fillUserAndTeamOwnerDetails = async ( export const fillEntityTypeDetails = async (page: Page, entityType: string) => { await page.keyboard.press('Enter', { delay: 100 }); - await page.getByTestId('entity-type-select').click(); - await page.getByTitle(entityType, { exact: true }).nth(0).click(); + // The editor opens its list as soon as the cell does. + await page.getByRole('option', { name: entityType, exact: true }).click(); await clickInlineSave(page); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useGridEditController.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useGridEditController.ts index 64ef151ac333..3f3b65bb0bc1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/useGridEditController.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useGridEditController.ts @@ -445,9 +445,7 @@ function handleDeleteOrBackspace( // Check if we're in a custom editor (like tag selector, dropdown, etc.) const isInCustomEditor = !!( target.closest('.async-select-list') || - target.closest('.react-grid-select-dropdown') || - target.closest('.ant-select') || - target.closest('.ant-select-dropdown') + target.closest('.react-grid-select-dropdown') ); if (!isInInput && !isInCustomEditor) { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/EntityImport/BulkEntityImportPage/bulk-entity-import-page.less b/openmetadata-ui/src/main/resources/ui/src/pages/EntityImport/BulkEntityImportPage/bulk-entity-import-page.less index e16190572fff..abaf93c0d72b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/EntityImport/BulkEntityImportPage/bulk-entity-import-page.less +++ b/openmetadata-ui/src/main/resources/ui/src/pages/EntityImport/BulkEntityImportPage/bulk-entity-import-page.less @@ -26,62 +26,6 @@ min-width: 12rem; } -.bulk-edit-enum-select-dropdown.ant-select-dropdown, -.bulk-edit-custom-property-select-dropdown.ant-select-dropdown { - background: var(--tw-color-bg-surface); - border: 1px solid var(--tw-color-border-secondary); - border-radius: var(--om-radius-10); - box-shadow: 0 12px 32px var(--om-legacy-color-16-24-40-0-14); - isolation: isolate; - opacity: 1 !important; - overflow: hidden; - padding: var(--om-space-6); - transform: none !important; - z-index: var(--om-z-1500); - - &::before { - background: var(--tw-color-bg-surface); - border-radius: inherit; - content: ''; - inset: 0; - position: absolute; - z-index: var(--om-z-0); - } - - .rc-virtual-list { - background: var(--tw-color-bg-surface); - position: relative; - z-index: var(--om-z-1); - } - - .ant-select-item { - align-items: center; - background: var(--tw-color-bg-surface); - border-radius: var(--om-radius-md); - color: var(--tw-color-text-primary); - display: flex; - font-family: 'Geist Mono', 'JetBrains Mono', Menlo, monospace; - font-size: var(--om-font-size-13); - min-height: 38px; - padding: var(--om-space-8) var(--om-space-10); - } - - .ant-select-item-option-content { - line-height: 20px; - } - - .ant-select-item-option-active:not(.ant-select-item-option-disabled), - .ant-select-item-option-selected:not(.ant-select-item-option-disabled) { - background: var(--tw-color-bg-brand-primary); - color: var(--tw-color-text-primary); - font-weight: @font-medium; - } - - .ant-select-item-option-state { - color: var(--tw-color-fg-brand-primary); - } -} - .csv-import-page-stack { display: flex; flex-direction: column; @@ -607,41 +551,6 @@ flex-shrink: 0; } - .bulk-edit-enum-select { - min-width: 0; - width: 100%; - - &.ant-select-focused:not(.ant-select-disabled).ant-select:not( - .ant-select-customize-input - ) - .ant-select-selector { - border-color: transparent; - box-shadow: none; - } - - .ant-select-selector { - background: transparent; - border: none; - box-shadow: none; - height: 24px; - padding: 0; - } - - .ant-select-selection-item { - color: var(--tw-color-text-primary); - font-family: 'Geist Mono', 'JetBrains Mono', Menlo, monospace; - font-size: var(--om-font-size-12_5); - line-height: 24px; - padding-right: var(--om-space-20); - } - - .ant-select-arrow { - color: var(--tw-color-fg-quaternary); - margin-top: calc(-1 * var(--om-space-7)); - right: 0; - } - } - .bulk-edit-text-cell-editor { background: transparent; border: none; @@ -1291,24 +1200,6 @@ } } -.bulk-edit-custom-property-enum-select.ant-select { - width: 100%; - - .ant-select-selector { - border-color: var(--tw-color-border-secondary); - border-radius: 6px; - min-height: 36px; - } - - &.ant-select-focused:not(.ant-select-disabled).ant-select:not( - .ant-select-customize-input - ) - .ant-select-selector { - border-color: var(--tw-color-border-brand); - box-shadow: 0 0 0 3px rgba(21, 112, 239, 0.14); - } -} - .bulk-edit-custom-property-time-interval { display: flex; flex-wrap: wrap; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.test.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.test.tsx index 9c13cef1f327..8152a35f2071 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.test.tsx @@ -468,6 +468,33 @@ describe('CSV utils ClassBase', () => { expect(editor).toBeDefined(); }); + it('should commit the picked metric enum value and close the cell', async () => { + const editor = csvUtils.getEditor( + 'metricType', + EntityType.METRIC, + multipleOwner, + { usePlainTextEditor: true } + ); + const onRowChange = jest.fn(); + const onClose = jest.fn(); + + render( + <> + {editor({ + row: { metricType: 'COUNT' }, + column: { key: 'metricType' }, + onRowChange, + onClose, + } as unknown as Parameters[0])} + + ); + + fireEvent.click(await screen.findByRole('option', { name: 'SUM' })); + + expect(onRowChange).toHaveBeenCalledWith({ metricType: 'SUM' }, true); + expect(onClose).toHaveBeenCalledWith(true); + }); + it('should return editor for code column', () => { const editor = csvUtils.getEditor( 'code', @@ -950,14 +977,11 @@ describe('CSV utils ClassBase', () => { expect(await screen.findByText('Status')).toBeInTheDocument(); expect(screen.getByText('Markets')).toBeInTheDocument(); - expect( - document.querySelectorAll('.bulk-edit-custom-property-enum-select') - ).toHaveLength(2); - expect( - document.querySelectorAll( - '.bulk-edit-custom-property-enum-select.ant-select-multiple' - ) - ).toHaveLength(1); + expect(screen.getByTestId('status-enum-select')).toBeInTheDocument(); + expect(screen.getByTestId('markets-enum-select')).toBeInTheDocument(); + // A multi-select enum shows each stored value as a chip. + expect(screen.getByText('US')).toBeInTheDocument(); + expect(screen.getByText('EU')).toBeInTheDocument(); expect( document.querySelector('.bulk-edit-custom-property-option') ).not.toBeInTheDocument(); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.tsx index 497bfe784468..05b0636564d0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.tsx @@ -12,9 +12,11 @@ */ import { + Autocomplete, Box, ButtonUtility, Divider, + Select, TextArea, } from '@openmetadata/ui-core-components'; import { @@ -22,7 +24,6 @@ import { Bold01, BookOpen02, Check, - ChevronDown, Code01, Cube01, Globe02, @@ -34,11 +35,11 @@ import { Tag01, Type01, } from '@openmetadata/ui-core-components/icons'; -import Select from 'antd/lib/select'; import { isEmpty, startCase, toString } from 'lodash'; import { CSSProperties, FocusEvent, + Key, KeyboardEvent, lazy, MouseEvent, @@ -1476,37 +1477,33 @@ const InlineCustomPropertiesEditor = ({ ); const selectedValues = getCustomPropertyEnumValues(propertyValue); const isMultiSelect = isCustomPropertyEnumMultiSelect(customProperty); - const options = enumValues.map((option) => ({ - label: option, - value: option, - })); + const items = enumValues.map((option) => ({ id: option, label: option })); - const handleEnumChange = (selectedValue?: string | string[]) => { - let nextValue: string[]; - if (Array.isArray(selectedValue)) { - nextValue = selectedValue; - } else if (selectedValue) { - nextValue = [selectedValue]; - } else { - nextValue = []; - } - handleUpdateDraft(customProperty.name, nextValue); - }; + const handleInserted = (key: Key) => + handleUpdateDraft( + customProperty.name, + isMultiSelect ? [...selectedValues, String(key)] : [String(key)] + ); + + const handleCleared = (key: Key) => + handleUpdateDraft( + customProperty.name, + selectedValues.filter((value) => value !== String(key)) + ); return ( - ({ id: option.value, label: option.label }))} + popoverClassName="react-grid-select-dropdown" + selectedKey={value || null} + size="sm" + onSelectionChange={(key) => onChange(String(key))}> + {(item) => } + +); + const getCsvOwnerEditor: CSVEditorFactory = ({ column, entityType, @@ -2371,28 +2400,16 @@ const getCsvTiersEditor: CSVEditorFactory = ({ entityType, options }) => { return ( - + {renderGridEnumSelect({ + ariaLabel: startCase(column.key), + options: translatedEntityTypeOptions(), + testId: 'entity-type-select', + value: String(value ?? ''), + onChange: handleChange, + })} ); @@ -2705,20 +2718,26 @@ const getCsvMultiSelectEditor: CSVEditorFactory = ({ onClose(false)} onSave={() => onClose(true)}> - document.body} - menuItemSelectedIcon={} - optionFilterProp="label" - options={ + {renderGridEnumSelect({ + ariaLabel: startCase(column.key), + options: entityBulkEditConfigClassBase.getEnumColumnOptions(entityType)[ column.key - ] - } - popupClassName="bulk-edit-enum-select-dropdown" - size="small" - suffixIcon={} - value={value || undefined} - onChange={handleChange} - /> + ], + testId: `${column.key}-select`, + value: String(value ?? ''), + onChange: handleChange, + })} ); }; From 3c3864ff2430a9bb3377ab8d850f1477c9681a51 Mon Sep 17 00:00:00 2001 From: Harshit Shah Date: Thu, 15 Oct 2026 12:44:45 +0530 Subject: [PATCH 04/34] refactor(ui): build the classic notification alert form on react-hook-form AddNotificationPage was the last antd Form. It now reuses the observability alert form (useAlertFormData picks the notification fetch, save, breadcrumb and details path by alert type) and hides the trigger section notifications do not have. Removes the antd-only NAME_FIELD_RULES and VALIDATION_MESSAGES. --- .../AlertAiFormFieldsValidationUtils.test.ts | 2 +- .../AlertAiFormFieldsValidationUtils.ts | 2 +- .../ui/src/constants/Form.constants.ts | 7 - .../resources/ui/src/constants/constants.ts | 32 - .../AddNotificationPage.interface.ts | 14 - .../AddNotificationPage.test.tsx | 2 +- .../AddNotificationPage.tsx | 597 +----------------- .../hooks/useAlertFormData.ts | 75 ++- .../hooks/useObservabilityAlertForm.ts | 2 +- .../src/pages/TeamsPage/AddTeamForm.test.tsx | 4 - 10 files changed, 78 insertions(+), 659 deletions(-) delete mode 100644 openmetadata-ui/src/main/resources/ui/src/pages/AddNotificationPage/AddNotificationPage.interface.ts diff --git a/openmetadata-ui/src/main/resources/ui/src/components/observability/Alerts/AlertAiFormFieldsValidationUtils.test.ts b/openmetadata-ui/src/main/resources/ui/src/components/observability/Alerts/AlertAiFormFieldsValidationUtils.test.ts index 83bb608ffec0..e2394f669361 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/observability/Alerts/AlertAiFormFieldsValidationUtils.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/observability/Alerts/AlertAiFormFieldsValidationUtils.test.ts @@ -40,7 +40,7 @@ describe('AlertAiFormFieldsValidationUtils', () => { }); }); - describe('name follows the classic NAME_FIELD_RULES', () => { + describe('name follows NAME_FIELD_REGISTER_RULES', () => { const validate = (displayName: string) => validateAlertAiForm( { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/observability/Alerts/AlertAiFormFieldsValidationUtils.ts b/openmetadata-ui/src/main/resources/ui/src/components/observability/Alerts/AlertAiFormFieldsValidationUtils.ts index 92adbad4c46f..579c4470000a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/observability/Alerts/AlertAiFormFieldsValidationUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/observability/Alerts/AlertAiFormFieldsValidationUtils.ts @@ -51,7 +51,7 @@ const getRequiredFieldMessage = (fieldText: string, t: TFunction) => const NAME_MAX_LENGTH = 128; -// Mirrors the classic alert forms' NAME_FIELD_RULES (constants/Form.constants.ts) +// Mirrors NAME_FIELD_REGISTER_RULES (constants/Form.constants.ts) // so the AI form accepts and rejects exactly the same names. const setNameRuleError = ( errors: AlertAiFormValidationErrors, diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/Form.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/Form.constants.ts index 53572068a3bd..88380ae13eae 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/Form.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/Form.constants.ts @@ -33,17 +33,10 @@ const NAME_SIZE_MESSAGE = sizeMessage('label.name'); const NAME_PATTERN_MESSAGE = String(i18n.t('message.entity-name-validation')); const DISPLAY_NAME_SIZE_MESSAGE = sizeMessage('label.display-name'); -export const NAME_FIELD_RULES: EntityNameValidationRule[] = [ - { required: true, message: NAME_REQUIRED_MESSAGE }, - { min: 1, max: ENTITY_NAME_MAX_LENGTH, message: NAME_SIZE_MESSAGE }, - { pattern: ENTITY_NAME_REGEX, message: NAME_PATTERN_MESSAGE }, -]; - export const DISPLAY_NAME_FIELD_RULES: EntityNameValidationRule[] = [ { min: 1, max: ENTITY_NAME_MAX_LENGTH, message: DISPLAY_NAME_SIZE_MESSAGE }, ]; -/** react-hook-form counterpart of NAME_FIELD_RULES. */ export const NAME_FIELD_REGISTER_RULES = { required: NAME_REQUIRED_MESSAGE, maxLength: { value: ENTITY_NAME_MAX_LENGTH, message: NAME_SIZE_MESSAGE }, diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts index c48c52a097a2..57d387ae81ab 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts @@ -469,38 +469,6 @@ export const ENTITY_REFERENCE_OPTIONS = [ }, ]; -export const VALIDATION_MESSAGES = { - required: i18n.t('message.field-text-is-required', { - fieldText: '${label}', - }), - types: { - email: i18n.t('message.entity-is-not-valid', { - entity: '${label}', - }), - }, - whitespace: i18n.t('message.field-text-is-required', { - fieldText: '${label}', - }), - string: { - range: i18n.t('message.entity-size-in-between', { - entity: '${label}', - min: '${min}', - max: '${max}', - }), - }, - number: { - range: i18n.t('message.entity-size-in-between', { - entity: '${label}', - min: '${min}', - max: '${max}', - }), - min: i18n.t('message.entity-size-less-than', { - entity: '${label}', - min: '${min}', - }), - }, -}; - export const ERROR_MESSAGE = { alreadyExist: 'already exists', mutuallyExclusive: 'mutually exclusive', diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AddNotificationPage/AddNotificationPage.interface.ts b/openmetadata-ui/src/main/resources/ui/src/pages/AddNotificationPage/AddNotificationPage.interface.ts deleted file mode 100644 index 8f8ba5832e40..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AddNotificationPage/AddNotificationPage.interface.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright 2022 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. - */ - -export type { AddAlertPageLoadingState } from '../../utils/AlertsClassBase.interface'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AddNotificationPage/AddNotificationPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AddNotificationPage/AddNotificationPage.test.tsx index c1ce13db355d..b97ef3cfd316 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AddNotificationPage/AddNotificationPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AddNotificationPage/AddNotificationPage.test.tsx @@ -100,7 +100,7 @@ jest.mock( ); jest.mock( - '../../components/Alerts/DestinationFormItem/DestinationFormItemFormBridge', + '../../components/Alerts/DestinationFormItem/DestinationFormItem.component', () => jest .fn() diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AddNotificationPage/AddNotificationPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AddNotificationPage/AddNotificationPage.tsx index 6602fa755f5c..f5660d11085e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AddNotificationPage/AddNotificationPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AddNotificationPage/AddNotificationPage.tsx @@ -10,312 +10,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - Box, - Button, - Card, - Divider, - Grid, - SkeletonParagraph, - Typography, -} from '@openmetadata/ui-core-components'; -import { Form, Input } from 'antd'; -import { useForm } from 'antd/lib/form/Form'; -import { isEmpty, isUndefined, pickBy } from 'lodash'; -import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; +import { Card, Typography } from '@openmetadata/ui-core-components'; +import { isEmpty } from 'lodash'; import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; -import AlertFormSourceItem from '../../components/Alerts/AlertFormSourceItem/AlertFormSourceItem'; -import DestinationFormItemFormBridge, { - DestinationFormFieldRegistrar, -} from '../../components/Alerts/DestinationFormItem/DestinationFormItemFormBridge'; -import ObservabilityFormFiltersItem from '../../components/Alerts/ObservabilityFormFiltersItem/ObservabilityFormFiltersItem'; import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; -import InlineAlert from '../../components/common/InlineAlert/InlineAlert'; import Loader from '../../components/common/Loader/Loader'; import ResizablePanels from '../../components/common/ResizablePanels/ResizablePanels'; -import RichTextEditor from '../../components/common/RichTextEditor/RichTextEditor'; -import TitleBreadcrumb from '../../components/common/TitleBreadcrumb/TitleBreadcrumb.component'; -import { AlertAiFormValidationErrors } from '../../components/observability/Alerts/AlertAiFormFields.interface'; -import { validateAlertAiForm } from '../../components/observability/Alerts/AlertAiFormFieldsValidationUtils'; -import { - PAGE_SIZE_LARGE, - ROUTES, - VALIDATION_MESSAGES, -} from '../../constants/constants'; -import { NAME_FIELD_RULES } from '../../constants/Form.constants'; -import { - GlobalSettingOptions, - GlobalSettingsMenuCategory, -} from '../../constants/GlobalSettings.constants'; -import { useLimitStore } from '../../context/LimitsProvider/useLimitsStore'; -import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider'; -import { OperationPermission } from '../../context/PermissionProvider/PermissionProvider.interface'; import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; -import { ResourceEntity } from '../../enums/permissions.enum'; -import { - NotificationTemplate, - ProviderType, -} from '../../generated/entity/events/notificationTemplate'; -import { AlertType as CapabilitiesAlertType } from '../../generated/events/api/alertCapabilitiesRequest'; -import { CreateEventSubscription } from '../../generated/events/api/createEventSubscription'; -import { - AlertType, - EventSubscription, -} from '../../generated/events/eventSubscription'; -import { FilterResourceDescriptor } from '../../generated/events/filterResourceDescriptor'; +import { ProviderType } from '../../generated/entity/events/notificationTemplate'; +import { AlertType } from '../../generated/events/eventSubscription'; import { withPageLayout } from '../../hoc/withPageLayout'; -import { - AlertSelectionProvider, - useAlertSelection, -} from '../../hooks/useAlertSelection'; -import { useApplicationStore } from '../../hooks/useApplicationStore'; -import { useFqn } from '../../hooks/useFqn'; -import { - createNotificationAlert, - getAlertsFromName, - getResourceFunctions, - updateNotificationAlert, -} from '../../rest/alertsAPI'; -import { getAllNotificationTemplates } from '../../rest/notificationtemplateAPI'; -import { toCapabilitiesInput } from '../../utils/Alerts/AlertSelectionUtil'; -import { getAlertSourceChanges } from '../../utils/Alerts/ClassicAlertForm.utils'; -import alertsClassBase from '../../utils/AlertsClassBase'; -import { getEntityName } from '../../utils/EntityNameUtils'; -import { getDerivedPermissionFlags } from '../../utils/PermissionDerivation'; -import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; -import { - getNotificationAlertDetailsPath, - getSettingPath, -} from '../../utils/RouterUtils'; -import { showErrorToast } from '../../utils/ToastUtils'; -import { - ModifiedCreateEventSubscription, - ModifiedEventSubscription, -} from '../AddObservabilityPage/AddObservabilityPage.interface'; -import { AddAlertPageLoadingState } from './AddNotificationPage.interface'; - -// One array for "nothing selected", so what depends on the selection does not change every render. -const NO_SOURCES: string[] = []; +import ObservabilityAlertForm from '../AddObservabilityPage/components/ObservabilityAlertForm'; +import { useObservabilityAlertForm } from '../AddObservabilityPage/hooks/useObservabilityAlertForm'; const AddNotificationPage = () => { - const [form] = useForm(); - const [ruleValidationErrors, setRuleValidationErrors] = - useState({}); - const navigate = useNavigate(); - const { fqn } = useFqn(); const { t } = useTranslation(); - const getRuleValidationErrors = () => - pickBy(validateAlertAiForm(form.getFieldsValue(true), t), (_, path) => - path.startsWith('input.') - ); - const { setInlineAlertDetails, inlineAlertDetails, currentUser } = - useApplicationStore(); - const { getResourceLimit } = useLimitStore(); - - const [loadingState, setLoadingState] = useState({ - alerts: false, - functions: false, - templates: false, - }); - const [entityFunctions, setEntityFunctions] = useState< - FilterResourceDescriptor[] - >([]); - const { getResourcePermission } = usePermissionProvider(); - const [isButtonLoading, setIsButtonLoading] = useState(false); - const [alert, setAlert] = useState(); - const [initialData, setInitialData] = useState(); - const [templates, setTemplates] = useState([]); - const [templateResourcePermission, setTemplateResourcePermission] = - useState(DEFAULT_ENTITY_PERMISSION); - - const isSystemProvider = useMemo( - () => alert?.provider === ProviderType.System, - [alert] - ); - - const breadcrumb = useMemo( - () => [ - { - name: t('label.setting-plural'), - url: ROUTES.SETTINGS, - }, - { - name: t('label.notification-plural'), - url: getSettingPath(GlobalSettingsMenuCategory.NOTIFICATIONS), - }, - { - name: t('label.alert-plural'), - url: getSettingPath( - GlobalSettingsMenuCategory.NOTIFICATIONS, - GlobalSettingOptions.ALERTS - ), - }, - { - name: fqn - ? t('label.edit-entity', { entity: t('label.alert') }) - : t('label.create-entity', { entity: t('label.alert') }), - url: '', - }, - ], - [fqn] - ); - - const fetchAlert = async () => { - try { - setLoadingState((state) => ({ ...state, alerts: true })); - - const response: EventSubscription = await getAlertsFromName(fqn); - const modifiedAlertData: ModifiedEventSubscription = - alertsClassBase.getModifiedAlertDataForForm(response); - - setInitialData(response); - setAlert(modifiedAlertData); - } catch { - showErrorToast( - t('server.entity-fetch-error', { entity: t('label.alert') }), - fqn - ); - } finally { - setLoadingState((state) => ({ ...state, alerts: false })); - } - }; - - const fetchFunctions = async () => { - try { - setLoadingState((state) => ({ ...state, functions: true })); - - const entityFunctions = await getResourceFunctions(); - - setEntityFunctions(entityFunctions.data); - } catch { - showErrorToast( - t('server.entity-fetch-error', { entity: t('label.config') }) - ); - } finally { - setLoadingState((state) => ({ ...state, functions: false })); - } - }; - - useEffect(() => { - fetchFunctions(); - if (!fqn) { - return; - } - fetchAlert(); - }, [fqn]); - - const isEditMode = useMemo(() => !isEmpty(fqn), [fqn]); - - const handleSave = useCallback( - async (data: ModifiedCreateEventSubscription) => { - try { - setIsButtonLoading(true); - - await alertsClassBase.handleAlertSave({ - data, - fqn, - initialData, - currentUser, - createAlertAPI: createNotificationAlert, - updateAlertAPI: updateNotificationAlert, - afterSaveAction: async (fqn: string) => { - !fqn && (await getResourceLimit('eventsubscription', true, true)); - navigate(getNotificationAlertDetailsPath(fqn)); - }, - setInlineAlertDetails, - }); - } catch { - // Error handling done in "handleAlertSave" - } finally { - setIsButtonLoading(false); - } - }, - [fqn, navigate, initialData, currentUser] - ); - - const resources = - Form.useWatch(['resources'], form) ?? - NO_SOURCES; - const destinations = Form.useWatch('destinations', form); - const timeout = Form.useWatch('timeout', form); - const readTimeout = Form.useWatch('readTimeout', form); - const chosenSoFar = Form.useWatch('input', form); - const values = Form.useWatch([], form); - const capabilitiesInput = useMemo( - () => toCapabilitiesInput(chosenSoFar), - [chosenSoFar] - ); - const selection = useAlertSelection({ - alertType: CapabilitiesAlertType.Notification, - sources: resources, - input: capabilitiesInput, - catalog: entityFunctions, + const formState = useObservabilityAlertForm({ + alertType: AlertType.Notification, }); - const { supportedFilters } = selection.support; + const { alert, isEditMode, isLoading } = formState; - const shouldShowFiltersSection = useMemo( - () => (isEmpty(resources) ? true : !isEmpty(supportedFilters)), - [resources, supportedFilters] - ); - - const extraFormWidgets = useMemo( - () => alertsClassBase.getAddAlertFormExtraWidgets(), - [] - ); - - const extraFormButtons = useMemo( - () => alertsClassBase.getAddAlertFormExtraButtons(), - [] - ); - - const fetchTemplates = useCallback(async () => { - setLoadingState((state) => ({ ...state, templates: true })); - try { - const permission = await getResourcePermission( - ResourceEntity.NOTIFICATION_TEMPLATE - ); - - setTemplateResourcePermission(permission); - - if (getDerivedPermissionFlags(permission).canViewAll) { - const { data } = await getAllNotificationTemplates({ - limit: PAGE_SIZE_LARGE, - provider: ProviderType.User, - }); - - setTemplates(data); - } - } catch { - showErrorToast( - t('server.entity-fetch-error', { entity: t('label.template-plural') }) - ); - } finally { - setLoadingState((state) => ({ ...state, templates: false })); - } - }, []); - - useEffect(() => { - if (!isEmpty(extraFormWidgets)) { - fetchTemplates(); - } - }, [extraFormWidgets]); - - const isLoading = useMemo( - () => Object.values(loadingState).some((val) => val), - [loadingState] - ); - - const shouldShowLoader = useMemo( - () => isLoading || (isEditMode && isEmpty(alert)), - [isLoading, isEditMode, alert] - ); - - if (shouldShowLoader) { + if (isLoading || (isEditMode && isEmpty(alert))) { return ; } - if (isSystemProvider) { + if (alert?.provider === ProviderType.System) { return ( @@ -335,300 +54,16 @@ const AddNotificationPage = () => { children: ( - - - - - - - - {t(`label.${isEditMode ? 'edit' : 'add'}-entity`, { - entity: t('label.alert'), - })} - - {t('message.alerts-description')} - - - - - className="alerts-notification-form" - form={form} - initialValues={{ - ...alert, - displayName: getEntityName(alert), - resources: alert?.filteringRules?.resources, - }} - validateMessages={VALIDATION_MESSAGES} - onFinish={handleSave} - onFinishFailed={() => - setRuleValidationErrors(getRuleValidationErrors()) - }> - {isLoading ? ( - - ) : ( - - - - - - - - - - - - - - - - { - setRuleValidationErrors({}); - Object.entries( - getAlertSourceChanges(next, previous) - ).forEach(([key, value]) => - form.setFieldValue(key, value) - ); - }} - /> - - - {shouldShowFiltersSection && ( - <> - - - - - { - const data = - typeof next === 'function' - ? next(form.getFieldsValue(true)) - : next; - form.setFieldValue('input', data.input); - if (!isEmpty(ruleValidationErrors)) { - setRuleValidationErrors( - getRuleValidationErrors() - ); - } - }} - /> - - - - )} - - - - - ( - - )} - values={{ - destinations, - readTimeout, - timeout, - }} - onChange={(values) => { - // Each shared field must be replaced at its root. Ant's - // bulk setter deep-merges destination array entries and - // would restore config removed by a type change. - Object.entries(values).forEach( - ([name, value]) => - form.setFieldValue(name, value) - ); - }} - /> - - - {!isEmpty(extraFormWidgets) && ( - <> - {Object.entries(extraFormWidgets).map( - ([name, Widget]) => ( - - - - - - - Object.entries(changes).forEach( - ([key, value]) => - form.setFieldValue(key, value) - ) - } - /> - - - ) - )} - - )} - - - - )} - - - - + {/* Notifications have no trigger section. */} + ), minWidth: 700, flex: 0.7, - wrapInCard: false, }} pageTitle={t('label.add-entity', { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AddObservabilityPage/hooks/useAlertFormData.ts b/openmetadata-ui/src/main/resources/ui/src/pages/AddObservabilityPage/hooks/useAlertFormData.ts index b3bc0fdd2248..182013d78fc3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AddObservabilityPage/hooks/useAlertFormData.ts +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AddObservabilityPage/hooks/useAlertFormData.ts @@ -15,11 +15,24 @@ import { isEmpty } from 'lodash'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; +import { ROUTES } from '../../../constants/constants'; +import { + GlobalSettingOptions, + GlobalSettingsMenuCategory, +} from '../../../constants/GlobalSettings.constants'; import { useLimitStore } from '../../../context/LimitsProvider/useLimitsStore'; import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider'; -import { EventSubscription } from '../../../generated/events/eventSubscription'; +import { + AlertType, + EventSubscription, +} from '../../../generated/events/eventSubscription'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { useFqn } from '../../../hooks/useFqn'; +import { + createNotificationAlert, + getAlertsFromName, + updateNotificationAlert, +} from '../../../rest/alertsAPI'; import { createObservabilityAlert, getObservabilityAlertByFQN, @@ -27,6 +40,10 @@ import { } from '../../../rest/observabilityAPI'; import alertsClassBase from '../../../utils/AlertsClassBase'; import observabilityRouterClassBase from '../../../utils/ObservabilityRouterClassBase'; +import { + getNotificationAlertDetailsPath, + getSettingPath, +} from '../../../utils/RouterUtils'; import { ModifiedCreateEventSubscription, ModifiedEventSubscription, @@ -69,6 +86,7 @@ export function useAlertFormData({ const [saving, setSaving] = useState(false); const isEditMode = useMemo(() => !isEmpty(fqn), [fqn]); + const isNotification = alertType === AlertType.Notification; const extraFormWidgets = useMemo( () => alertsClassBase.getAddAlertFormExtraWidgets(), [] @@ -87,7 +105,9 @@ export function useAlertFormData({ try { setLoadingState((state) => ({ ...state, alerts: true })); - const observabilityAlert = await getObservabilityAlertByFQN(fqn); + const observabilityAlert = isNotification + ? await getAlertsFromName(fqn) + : await getObservabilityAlertByFQN(fqn); const modifiedAlertData = alertsClassBase.getModifiedAlertDataForForm(observabilityAlert); @@ -98,7 +118,7 @@ export function useAlertFormData({ } finally { setLoadingState((state) => ({ ...state, alerts: false })); } - }, [fqn]); + }, [fqn, isNotification]); useEffect(() => { if (!fqn) { @@ -109,14 +129,28 @@ export function useAlertFormData({ const breadcrumb = useMemo( () => [ - { - name: t('label.observability'), - url: '', - }, - { - name: t('label.alert-plural'), - url: observabilityRouterClassBase.getObservabilityAlertsListPath(), - }, + ...(isNotification + ? [ + { name: t('label.setting-plural'), url: ROUTES.SETTINGS }, + { + name: t('label.notification-plural'), + url: getSettingPath(GlobalSettingsMenuCategory.NOTIFICATIONS), + }, + { + name: t('label.alert-plural'), + url: getSettingPath( + GlobalSettingsMenuCategory.NOTIFICATIONS, + GlobalSettingOptions.ALERTS + ), + }, + ] + : [ + { name: t('label.observability'), url: '' }, + { + name: t('label.alert-plural'), + url: observabilityRouterClassBase.getObservabilityAlertsListPath(), + }, + ]), { name: fqn ? t('label.edit-entity', { entity: t('label.alert') }) @@ -124,7 +158,7 @@ export function useAlertFormData({ url: '', }, ], - [fqn, t] + [fqn, isNotification, t] ); const handleSave = useCallback( @@ -137,8 +171,12 @@ export function useAlertFormData({ fqn, initialData, currentUser, - createAlertAPI: createObservabilityAlert, - updateAlertAPI: updateObservabilityAlert, + createAlertAPI: isNotification + ? createNotificationAlert + : createObservabilityAlert, + updateAlertAPI: isNotification + ? updateNotificationAlert + : updateObservabilityAlert, afterSaveAction: async (savedFqn: string) => { if (!isEditMode) { await getResourceLimit('eventsubscription', true, true); @@ -151,9 +189,11 @@ export function useAlertFormData({ } navigate( - observabilityRouterClassBase.getObservabilityAlertDetailsPath( - savedFqn - ) + isNotification + ? getNotificationAlertDetailsPath(savedFqn) + : observabilityRouterClassBase.getObservabilityAlertDetailsPath( + savedFqn + ) ); }, setInlineAlertDetails, @@ -171,6 +211,7 @@ export function useAlertFormData({ getResourceLimit, initialData, isEditMode, + isNotification, navigate, setInlineAlertDetails, ] diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AddObservabilityPage/hooks/useObservabilityAlertForm.ts b/openmetadata-ui/src/main/resources/ui/src/pages/AddObservabilityPage/hooks/useObservabilityAlertForm.ts index a443fc9d7549..ed178241003e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AddObservabilityPage/hooks/useObservabilityAlertForm.ts +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AddObservabilityPage/hooks/useObservabilityAlertForm.ts @@ -60,7 +60,7 @@ export function useObservabilityAlertForm({ resources: [], destinations: [], input: {}, - alertType: AlertType.Observability, + alertType: options.alertType ?? AlertType.Observability, provider: ProviderType.User, timeout: 10, readTimeout: DEFAULT_READ_TIMEOUT, diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TeamsPage/AddTeamForm.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TeamsPage/AddTeamForm.test.tsx index 76629fa7502a..f9d05700394c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TeamsPage/AddTeamForm.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TeamsPage/AddTeamForm.test.tsx @@ -17,10 +17,6 @@ import AddTeamForm from './AddTeamForm'; const mockCancel = jest.fn(); const mockSave = jest.fn(); -jest.mock('../../constants/constants', () => ({ - VALIDATION_MESSAGES: [], -})); - jest.mock('../../utils/RouterUtils', () => ({ getDomainPath: jest.fn(), })); From 1b72f0b963db85738d8a73fa7bbda66860bd036d Mon Sep 17 00:00:00 2001 From: Harshit Shah Date: Thu, 15 Oct 2026 12:47:04 +0530 Subject: [PATCH 05/34] refactor(ui): serve notification alerts from the observability alert page Rather than a second page importing another page's form, the notification add/edit routes render AddObservabilityPage with alertType=Notification, which hides triggers and keeps the system-alert guard. Deletes AddNotificationPage. --- .../AppRouter/SettingsRouter.test.tsx | 2 +- .../components/AppRouter/SettingsRouter.tsx | 11 +- .../Alerts/AlertAiFormFieldsPureUtils.ts | 4 +- .../AddNotificationPage.test.tsx | 307 ------------------ .../AddNotificationPage.tsx | 81 ----- .../AddObservabilityPage.interface.ts | 2 + .../AddObservabilityPage.test.tsx | 40 +++ .../AddObservabilityPage.tsx | 34 +- 8 files changed, 81 insertions(+), 400 deletions(-) delete mode 100644 openmetadata-ui/src/main/resources/ui/src/pages/AddNotificationPage/AddNotificationPage.test.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/pages/AddNotificationPage/AddNotificationPage.tsx diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/SettingsRouter.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/SettingsRouter.test.tsx index 5f85688f5348..4174d42d14dc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/SettingsRouter.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/SettingsRouter.test.tsx @@ -38,7 +38,7 @@ jest.mock('../observability/Alerts/AlertDetailsPage', () => ({ ), })); -jest.mock('../../pages/AddNotificationPage/AddNotificationPage', () => ({ +jest.mock('../../pages/AddObservabilityPage/AddObservabilityPage', () => ({ __esModule: true, default: jest.fn().mockReturnValue(
AddNotificationPage
), })); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/SettingsRouter.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/SettingsRouter.tsx index 3de20cc57912..4eb0235393a7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/SettingsRouter.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/SettingsRouter.tsx @@ -23,6 +23,7 @@ import { usePermissionProvider } from '../../context/PermissionProvider/Permissi import { ResourceEntity } from '../../enums/permissions.enum'; import { Operation } from '../../generated/entity/policies/accessControl/resourcePermission'; import { TeamType } from '../../generated/entity/teams/team'; +import { AlertType } from '../../generated/events/eventSubscription'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useIsAiMode } from '../../hooks/useAppMode'; import { isLoginConfigurationApplicable } from '../../utils/AuthProvider.util'; @@ -37,9 +38,9 @@ import { NOTIFICATION_ALERT_KIND } from '../observability/Alerts/alertKinds'; import AdminProtectedRoute from './AdminProtectedRoute'; import { withPageSuspenseFallback } from './withSuspenseFallback'; -const AddNotificationPage = withPageSuspenseFallback( +const AddObservabilityPage = withPageSuspenseFallback( React.lazy( - () => import('../../pages/AddNotificationPage/AddNotificationPage') + () => import('../../pages/AddObservabilityPage/AddObservabilityPage') ) ); @@ -544,7 +545,8 @@ const SettingsRouter = () => { /> { /> ({ - __esModule: true, - default: { - handleAlertSave: (...args: unknown[]) => mockHandleAlertSave(...args), - getModifiedAlertDataForForm: (...args: unknown[]) => - mockGetModifiedAlertDataForForm(...args), - getAddAlertFormExtraWidgets: jest.fn().mockReturnValue({}), - getSourceNameSearch: jest.fn().mockReturnValue({}), - getAddAlertFormExtraButtons: jest.fn().mockReturnValue({}), - }, -})); - -jest.mock('../../context/PermissionProvider/PermissionProvider', () => ({ - usePermissionProvider: jest.fn(() => ({ - getResourcePermission: mockGetResourcePermission, - })), -})); - -jest.mock('../../rest/notificationtemplateAPI', () => ({ - getAllNotificationTemplates: jest.fn().mockResolvedValue({ data: [] }), -})); - -jest.mock('../../rest/alertsAPI', () => ({ - getAlertsFromName: jest.fn().mockImplementation(() => - Promise.resolve({ - name: 'ActivityFeedAlert', - }) - ), - getResourceFunctions: jest.fn(), - updateNotificationAlert: jest.fn().mockImplementation(() => - Promise.resolve({ - id: 'ActivityFeedAlert', - data: [], - }) - ), - createNotificationAlert: jest.fn().mockImplementation(() => - Promise.resolve({ - alert: 'Notification', - }) - ), -})); - -jest.mock('../../utils/RouterUtils', () => ({ - getNotificationAlertDetailsPath: jest.fn(), - getSettingPath: jest.fn(), -})); - -jest.mock('../../hoc/withPageLayout', () => ({ - withPageLayout: jest.fn().mockImplementation((Component) => Component), -})); - -jest.mock('../../components/common/ResizablePanels/ResizablePanels', () => - jest.fn().mockImplementation(({ firstPanel, secondPanel }) => ( - <> -
{firstPanel.children}
-
{secondPanel.children}
- - )) -); - -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useNavigate: jest.fn().mockImplementation(() => mockNavigate), -})); - -jest.mock('../../hooks/useFqn', () => ({ - useFqn: jest.fn().mockReturnValue({ fqn: '' }), -})); - -jest.mock( - '../../components/Alerts/AlertFormSourceItem/AlertFormSourceItem', - () => - jest - .fn() - .mockImplementation(() => ( -
Source Select
- )) -); - -jest.mock( - '../../components/Alerts/DestinationFormItem/DestinationFormItem.component', - () => - jest - .fn() - .mockImplementation(() => ( -
Destination Select
- )) -); - -jest.mock( - '../../components/Alerts/ObservabilityFormFiltersItem/ObservabilityFormFiltersItem', - () => jest.fn().mockImplementation(() =>
Filters
) -); - -const mockProps = { - pageTitle: 'add-notifications', -}; - -describe('AddNotificationPage', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should render Add Notification Page', async () => { - await act(async () => { - render(, { - wrapper: MemoryRouter, - }); - }); - - expect( - await screen.findByText('label.notification-plural') - ).toBeInTheDocument(); - }); - - it('should display the correct breadcrumb', async () => { - await act(async () => { - render(, { - wrapper: MemoryRouter, - }); - }); - const breadcrumbLinks = screen.getAllByTestId('breadcrumb-link'); - - expect(breadcrumbLinks[0]).toHaveTextContent('label.setting-plural'); - expect(breadcrumbLinks[1]).toHaveTextContent('label.notification-plural'); - expect(breadcrumbLinks[2]).toHaveTextContent('label.alert-plural'); - expect(breadcrumbLinks[3]).toHaveTextContent('label.create-entity'); - }); - - it('should display SubTitle', async () => { - await act(async () => { - render(, { - wrapper: MemoryRouter, - }); - }); - - expect( - await screen.findByText(/message.alerts-description/) - ).toBeInTheDocument(); - }); - - it('should render Add alert button', async () => { - await act(async () => { - render(, { - wrapper: MemoryRouter, - }); - }); - - expect(await screen.findByText(/label.create-entity/)).toBeInTheDocument(); - }); - - it('should navigate back when the cancel button is clicked', async () => { - await act(async () => { - render(, { - wrapper: MemoryRouter, - }); - }); - - const cancelButton = screen.getByTestId('cancel-button'); - - await act(async () => { - fireEvent.click(cancelButton); - }); - - expect(mockNavigate).toHaveBeenCalledWith(-1); - }); - - it('should render form field for name', async () => { - await act(async () => { - render(, { - wrapper: MemoryRouter, - }); - }); - - await screen.findByText('label.notification-plural'); - - expect(screen.getByPlaceholderText('label.name')).toBeInTheDocument(); - }); - - it('should render save button', async () => { - await act(async () => { - render(, { - wrapper: MemoryRouter, - }); - }); - - await screen.findByText('label.notification-plural'); - - const saveButton = screen.getByTestId('save-button'); - - expect(saveButton).toBeInTheDocument(); - expect(saveButton).toHaveTextContent('label.save'); - }); - - it('should render AlertFormSourceItem component', async () => { - await act(async () => { - render(, { - wrapper: MemoryRouter, - }); - }); - - await screen.findByText('label.notification-plural'); - - expect(screen.getByTestId('source-select')).toBeInTheDocument(); - }); - - it('should render DestinationFormItem component', async () => { - await act(async () => { - render(, { - wrapper: MemoryRouter, - }); - }); - - await screen.findByText('label.notification-plural'); - - expect( - screen.getByTestId('destination-category-select') - ).toBeInTheDocument(); - }); - - describe('notification template permissions', () => { - const renderWithExtraWidgets = async () => { - const { default: alertsClassBase } = await import( - '../../utils/AlertsClassBase' - ); - ( - alertsClassBase.getAddAlertFormExtraWidgets as jest.Mock - ).mockReturnValueOnce({ template: jest.fn() }); - - await act(async () => { - render(, { - wrapper: MemoryRouter, - }); - }); - }; - - it('should fetch templates when ViewAll is granted on the template resource', async () => { - mockGetResourcePermission.mockResolvedValue({ ViewAll: true }); - - await renderWithExtraWidgets(); - - expect(getAllNotificationTemplates).toHaveBeenCalled(); - }); - - it('should not fetch templates when ViewAll is denied on the template resource', async () => { - mockGetResourcePermission.mockResolvedValue({ ViewBasic: true }); - - await renderWithExtraWidgets(); - - expect(getAllNotificationTemplates).not.toHaveBeenCalled(); - }); - }); - - it('should call getAddAlertFormExtraWidgets from alertsClassBase', async () => { - const { default: alertsClassBase } = await import( - '../../utils/AlertsClassBase' - ); - - await act(async () => { - render(, { - wrapper: MemoryRouter, - }); - }); - - await screen.findByText('label.notification-plural'); - - expect(alertsClassBase.getAddAlertFormExtraWidgets).toHaveBeenCalled(); - }); - - it('should call getAddAlertFormExtraButtons from alertsClassBase', async () => { - const { default: alertsClassBase } = await import( - '../../utils/AlertsClassBase' - ); - - await act(async () => { - render(, { - wrapper: MemoryRouter, - }); - }); - - await screen.findByText('label.notification-plural'); - - expect(alertsClassBase.getAddAlertFormExtraButtons).toHaveBeenCalled(); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AddNotificationPage/AddNotificationPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AddNotificationPage/AddNotificationPage.tsx deleted file mode 100644 index f5660d11085e..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AddNotificationPage/AddNotificationPage.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2022 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 { Card, Typography } from '@openmetadata/ui-core-components'; -import { isEmpty } from 'lodash'; -import { useTranslation } from 'react-i18next'; -import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; -import Loader from '../../components/common/Loader/Loader'; -import ResizablePanels from '../../components/common/ResizablePanels/ResizablePanels'; -import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; -import { ProviderType } from '../../generated/entity/events/notificationTemplate'; -import { AlertType } from '../../generated/events/eventSubscription'; -import { withPageLayout } from '../../hoc/withPageLayout'; -import ObservabilityAlertForm from '../AddObservabilityPage/components/ObservabilityAlertForm'; -import { useObservabilityAlertForm } from '../AddObservabilityPage/hooks/useObservabilityAlertForm'; - -const AddNotificationPage = () => { - const { t } = useTranslation(); - const formState = useObservabilityAlertForm({ - alertType: AlertType.Notification, - }); - const { alert, isEditMode, isLoading } = formState; - - if (isLoading || (isEditMode && isEmpty(alert))) { - return ; - } - - if (alert?.provider === ProviderType.System) { - return ( - - - {t('message.system-alert-edit-message')} - - - ); - } - - return ( - - - {/* Notifications have no trigger section. */} - - - - ), - minWidth: 700, - flex: 0.7, - wrapInCard: false, - }} - pageTitle={t('label.add-entity', { - entity: t('label.notification-alert'), - })} - secondPanel={{ - children: <>, - minWidth: 0, - className: 'content-resizable-panel-container', - }} - /> - ); -}; - -export default withPageLayout(AddNotificationPage); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AddObservabilityPage/AddObservabilityPage.interface.ts b/openmetadata-ui/src/main/resources/ui/src/pages/AddObservabilityPage/AddObservabilityPage.interface.ts index 0f98a99ca5ef..50f74a5a28ce 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AddObservabilityPage/AddObservabilityPage.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AddObservabilityPage/AddObservabilityPage.interface.ts @@ -105,4 +105,6 @@ export interface UseAlertFormDataReturn { export interface AddObservabilityPageProps { pageTitle: string; + /** Notification alerts reuse this page; they have no trigger section. */ + alertType?: AlertType; } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AddObservabilityPage/AddObservabilityPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AddObservabilityPage/AddObservabilityPage.test.tsx index 5afda08dd85b..0da75eb89555 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AddObservabilityPage/AddObservabilityPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AddObservabilityPage/AddObservabilityPage.test.tsx @@ -12,6 +12,7 @@ */ import { act, fireEvent, render, screen } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; +import { AlertType } from '../../generated/events/eventSubscription'; import AddObservabilityPage from './AddObservabilityPage'; const MOCK_DATA = [ @@ -81,6 +82,13 @@ jest.mock('../../rest/observabilityAPI', () => ({ ), })); +jest.mock('../../rest/alertsAPI', () => ({ + getAlertsFromName: jest.fn(), + getResourceFunctions: jest.fn(), + createNotificationAlert: jest.fn(), + updateNotificationAlert: jest.fn(), +})); + jest.mock('../../hoc/withPageLayout', () => ({ withPageLayout: jest.fn().mockImplementation((Component) => Component), })); @@ -306,4 +314,36 @@ describe('Add ObservabilityPage Alerts Page Tests', () => { 'typed-alert' ); }); + + describe('as the notification alert page', () => { + const notificationProps = { + ...mockProps, + alertType: AlertType.Notification, + }; + + it('should show the notification settings breadcrumb', async () => { + await act(async () => { + render(, { + wrapper: MemoryRouter, + }); + }); + const breadcrumbLinks = screen.getAllByTestId('breadcrumb-link'); + + expect(breadcrumbLinks[0]).toHaveTextContent('label.setting-plural'); + expect(breadcrumbLinks[1]).toHaveTextContent('label.notification-plural'); + expect(breadcrumbLinks[2]).toHaveTextContent('label.alert-plural'); + expect(breadcrumbLinks[3]).toHaveTextContent('label.create-entity'); + }); + + it('should not offer the trigger section', async () => { + await act(async () => { + render(, { + wrapper: MemoryRouter, + }); + }); + + expect(await screen.findByText('Filters')).toBeInTheDocument(); + expect(screen.queryByText('Triggers')).not.toBeInTheDocument(); + }); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AddObservabilityPage/AddObservabilityPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AddObservabilityPage/AddObservabilityPage.tsx index e1e08c6111ad..ac045fd10b02 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AddObservabilityPage/AddObservabilityPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AddObservabilityPage/AddObservabilityPage.tsx @@ -11,25 +11,42 @@ * limitations under the License. */ -import { Card } from '@openmetadata/ui-core-components'; +import { Card, Typography } from '@openmetadata/ui-core-components'; import { isEmpty } from 'lodash'; import { useTranslation } from 'react-i18next'; +import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import Loader from '../../components/common/Loader/Loader'; import ResizablePanels from '../../components/common/ResizablePanels/ResizablePanels'; +import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; +import { ProviderType } from '../../generated/entity/events/notificationTemplate'; +import { AlertType } from '../../generated/events/eventSubscription'; import { withPageLayout } from '../../hoc/withPageLayout'; import { AddObservabilityPageProps } from './AddObservabilityPage.interface'; import ObservabilityAlertForm from './components/ObservabilityAlertForm'; import { useObservabilityAlertForm } from './hooks/useObservabilityAlertForm'; -function AddObservabilityPage(_props: Readonly) { +function AddObservabilityPage({ + alertType = AlertType.Observability, +}: Readonly) { const { t } = useTranslation(); - const formState = useObservabilityAlertForm(); + const formState = useObservabilityAlertForm({ alertType }); const { alert, isEditMode, isLoading } = formState; + const isNotification = alertType === AlertType.Notification; if (isLoading || (isEditMode && isEmpty(alert))) { return ; } + if (alert?.provider === ProviderType.System) { + return ( + + + {t('message.system-alert-edit-message')} + + + ); + } + return ( ) { children: ( - + ), @@ -49,7 +71,9 @@ function AddObservabilityPage(_props: Readonly) { wrapInCard: false, }} pageTitle={t('label.add-entity', { - entity: t('label.observability'), + entity: t( + isNotification ? 'label.notification-alert' : 'label.observability' + ), })} secondPanel={{ children: <>, From 0ebbe9985e5ec346f124029e666c024516aa59c6 Mon Sep 17 00:00:00 2001 From: Harshit Shah Date: Thu, 15 Oct 2026 12:49:08 +0530 Subject: [PATCH 06/34] refactor(ui): drop the antd popup-container ConfigProviders No antd popup is left inside the drawers or the memory modal, so DrawerPopupContainerProvider and CreateMemoryModal's ConfigProvider no longer do anything. AntDConfigProvider stays: it still feeds the custom theme into antd's CSS variables, which the LESS and Collate's last antd form read. --- .../ActivityThreadPanel.tsx | 5 +- .../CreateMemoryModal.component.tsx | 312 +++++++++--------- .../RuleQueryBuilderField.component.tsx | 5 +- .../ColumnDetailPanel.component.tsx | 5 +- .../AddEditPersona.component.tsx | 5 +- .../DrawerPopupContainerProvider.tsx | 38 --- .../DrawerPopupContainerProvider/index.ts | 13 - .../ui/src/pages/TeamsPage/AddTeamForm.tsx | 5 +- 8 files changed, 161 insertions(+), 227 deletions(-) delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/DrawerPopupContainerProvider/DrawerPopupContainerProvider.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/DrawerPopupContainerProvider/index.ts diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel.tsx index a6b23d2639e4..9fa94b94170b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel.tsx @@ -12,7 +12,6 @@ */ import { SlideoutMenu, Tabs } from '@openmetadata/ui-core-components'; -import { DrawerPopupContainerProvider } from '../../common/DrawerPopupContainerProvider/DrawerPopupContainerProvider'; import classNames from 'classnames'; import { FC, useEffect, useState } from 'react'; @@ -64,7 +63,7 @@ const ActivityThreadPanel: FC = ({ } }}> - + <>
= ({
-
+
); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateMemoryModal/CreateMemoryModal.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateMemoryModal/CreateMemoryModal.component.tsx index 9ed113c48341..88380cb9ed71 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateMemoryModal/CreateMemoryModal.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateMemoryModal/CreateMemoryModal.component.tsx @@ -44,7 +44,6 @@ import { Share07, X, } from '@openmetadata/ui-core-components/icons'; -import { ConfigProvider } from 'antd'; import { AxiosError } from 'axios'; import { Dispatch, @@ -53,7 +52,6 @@ import { useCallback, useEffect, useMemo, - useRef, useState, } from 'react'; import { Control, useForm } from 'react-hook-form'; @@ -865,7 +863,6 @@ const CreateMemoryModal: FC = ({ currentUserName, }) => { const { t } = useTranslation(); - const modalContainerRef = useRef(null); const [isViewOnly, setIsViewOnly] = useState(viewOnly); const isEditMode = Boolean(memoryToEdit) && !isViewOnly; @@ -1154,174 +1151,167 @@ const CreateMemoryModal: FC = ({ width={600} onClose={handleClose}> -
- - modalContainerRef.current ?? document.body - }> - - {/* Sticky header */} - - - {/* Scrollable body */} -
- {/* Read-only banner for non-owners */} - + + {/* Sticky header */} + - {/* Inline error alert */} - {modalError && ( - setModalError('')} - /> - )} + {/* Scrollable body */} +
+ {/* Read-only banner for non-owners */} + + + {/* Inline error alert */} + {modalError && ( + setModalError('')} + /> + )} - {/* Section 1: Title — uses getField (TEXT) */} + {/* Section 1: Title — uses getField (TEXT) */} +
+ {getField(titleField)} +
+ + {/* Section 2: Memory content — FormField escape hatch for custom preview/edit tabs */} + + {({ field, fieldState }) => (
- {getField(titleField)} -
- - {/* Section 2: Memory content — FormField escape hatch for custom preview/edit tabs */} - - {({ field, fieldState }) => ( -
-
-
- +
+ + + + - - - - - -
- {!isViewOnly && ( - - )} -
- {renderMemoryContent(field)} - {fieldState.error?.message && ( - - {fieldState.error.message} - - )} + +
+ {!isViewOnly && ( + + )} +
+ {renderMemoryContent(field)} + {fieldState.error?.message && ( + + {fieldState.error.message} + )} -
- - {/* Section 3: Type — uses getField (SELECT) */} -
- {getField(memoryTypeField)}
+ )} +
+ + {/* Section 3: Type — uses getField (SELECT) */} +
+ {getField(memoryTypeField)} +
+ + {/* Section 4: Linked Data Assets */} + + + {/* Section 5: Metadata */} + - {/* Section 4: Linked Data Assets */} - - - {/* Section 5: Metadata */} - - - {memoryToEdit && ( - + {memoryToEdit && ( + - - {/* Sticky footer */} - - - -
+ )} +
+ + {/* Sticky footer */} + +
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/PersonaAIContext/ContextRuleEditor/RuleQueryBuilderField.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/PersonaAIContext/ContextRuleEditor/RuleQueryBuilderField.component.tsx index f4cc6139881e..11c7280fd121 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/PersonaAIContext/ContextRuleEditor/RuleQueryBuilderField.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/PersonaAIContext/ContextRuleEditor/RuleQueryBuilderField.component.tsx @@ -25,7 +25,6 @@ import { } from '../../../../utils/AdvancedSearchUtils'; import { getRuleFilterTree } from '../../../../utils/PersonaAIContextUtils'; import searchClassBase from '../../../../utils/SearchClassBase'; -import { DrawerPopupContainerProvider } from '../../../common/DrawerPopupContainerProvider/DrawerPopupContainerProvider'; import Loader from '../../../common/Loader/Loader'; import QueryBuilder from '../../../common/QueryBuilder/QueryBuilder'; import { PERSONA_BUTTON_PRESET } from '../../../common/QueryBuilder/QueryBuilderCanvas/QueryBuilderCanvas.constants'; @@ -109,7 +108,7 @@ export const RuleQueryBuilderField = ({ ); return ( - + <>
{/* Mounting before the fields arrive lets RAQB's sanitizer field-null any saved extension. rule. */} {enrichedFields ? ( @@ -130,6 +129,6 @@ export const RuleQueryBuilderField = ({ )}
-
+ ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/ColumnDetailPanel/ColumnDetailPanel.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/ColumnDetailPanel/ColumnDetailPanel.component.tsx index bcb069ac67fe..b83c75eaaa99 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/ColumnDetailPanel/ColumnDetailPanel.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/ColumnDetailPanel/ColumnDetailPanel.component.tsx @@ -26,7 +26,6 @@ import { XClose, } from '@openmetadata/ui-core-components/icons'; import { useId } from 'react'; -import { DrawerPopupContainerProvider } from '../../common/DrawerPopupContainerProvider/DrawerPopupContainerProvider'; import { AxiosError } from 'axios'; import classNames from 'classnames'; @@ -1088,7 +1087,7 @@ export const ColumnDetailPanel = ({ {columnTitle} - + <>
@@ -1110,7 +1109,7 @@ export const ColumnDetailPanel = ({
{renderDisplayNameModal()} -
+
{navFooter} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyData/Persona/AddEditPersona/AddEditPersona.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MyData/Persona/AddEditPersona/AddEditPersona.component.tsx index 14f0c92d3bf1..2b3f15b48dd5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MyData/Persona/AddEditPersona/AddEditPersona.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyData/Persona/AddEditPersona/AddEditPersona.component.tsx @@ -37,7 +37,6 @@ import { createPersona } from '../../../../rest/PersonaAPI'; import { getUserById } from '../../../../rest/userAPI'; import { getEntityName } from '../../../../utils/EntityNameUtils'; import { showErrorToast } from '../../../../utils/ToastUtils'; -import { DrawerPopupContainerProvider } from '../../../common/DrawerPopupContainerProvider/DrawerPopupContainerProvider'; import RichTextEditor from '../../../common/RichTextEditor/RichTextEditor'; import { UserSelectableList } from '../../../common/UserSelectableList/UserSelectableList.component'; import { UserTag } from '../../../common/UserTag/UserTag.component'; @@ -135,7 +134,7 @@ export const AddEditPersonaForm = ({ width={750} onCancel={onCancel} onOk={form.handleSubmit(handleSubmit)}> - + <> - + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DrawerPopupContainerProvider/DrawerPopupContainerProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DrawerPopupContainerProvider/DrawerPopupContainerProvider.tsx deleted file mode 100644 index 76d0f93c2685..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DrawerPopupContainerProvider/DrawerPopupContainerProvider.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 { ConfigProvider } from 'antd'; -import { PropsWithChildren } from 'react'; - -const PORTAL_CONTAINER_ID = 'drawer-popup-portal'; - -export const getPopupContainer = (): HTMLElement => { - let container = document.getElementById(PORTAL_CONTAINER_ID); - if (!container) { - container = document.createElement('div'); - container.id = PORTAL_CONTAINER_ID; - container.setAttribute('data-react-aria-top-layer', 'true'); - container.style.position = 'absolute'; - container.style.zIndex = '10001'; - document.body.appendChild(container); - } - - return container; -}; - -export const DrawerPopupContainerProvider = ({ - children, -}: PropsWithChildren) => ( - - {children} - -); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DrawerPopupContainerProvider/index.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/DrawerPopupContainerProvider/index.ts deleted file mode 100644 index b0c0f22979ec..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DrawerPopupContainerProvider/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * 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. - */ -export { DrawerPopupContainerProvider } from './DrawerPopupContainerProvider'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TeamsPage/AddTeamForm.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TeamsPage/AddTeamForm.tsx index fcc6896ba067..3efd132b5884 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TeamsPage/AddTeamForm.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TeamsPage/AddTeamForm.tsx @@ -31,7 +31,6 @@ import { RegisterOptions, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { DomainLabel } from '../../components/common/DomainLabel/DomainLabel.component'; import DomainSelectableList from '../../components/common/DomainSelectableList/DomainSelectableList.component'; -import { DrawerPopupContainerProvider } from '../../components/common/DrawerPopupContainerProvider/DrawerPopupContainerProvider'; import RichTextEditor from '../../components/common/RichTextEditor/RichTextEditor'; import { DISPLAY_NAME_FIELD_REGISTER_RULES, @@ -155,7 +154,7 @@ const AddTeamForm: React.FC = ({ width={750} onCancel={onCancel} onOk={form.handleSubmit(handleSubmit)}> - + <> = ({ )} - + ); }; From c08d8c7844c77ed06333060f7b9b22c44b0ddab9 Mon Sep 17 00:00:00 2001 From: Harshit Shah Date: Thu, 15 Oct 2026 12:53:00 +0530 Subject: [PATCH 07/34] chore(ui): lock Form, Select, Input and DatePicker out of antd All four reached zero, so they join MIGRATED_ANTD_COMPONENTS. Prunes the suppression baselines (antd symbols 148 -> 113, positional locators 560 -> 559) and swaps a test's stale antd Option mock for the core items the helper returns. --- .../eslint-rules/openmetadata-antd.test.mjs | 2 +- .../resources/ui/eslint-suppressions.json | 109 +----------------- .../src/main/resources/ui/eslint.config.mjs | 4 + .../eslint-rules/tests/corpus.test.mjs | 3 +- .../Bot/BotDetails/AuthMechanismForm.test.tsx | 9 +- 5 files changed, 10 insertions(+), 117 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/eslint-rules/openmetadata-antd.test.mjs b/openmetadata-ui/src/main/resources/ui/eslint-rules/openmetadata-antd.test.mjs index 19896d32e962..96b6f007184b 100644 --- a/openmetadata-ui/src/main/resources/ui/eslint-rules/openmetadata-antd.test.mjs +++ b/openmetadata-ui/src/main/resources/ui/eslint-rules/openmetadata-antd.test.mjs @@ -163,7 +163,7 @@ test('the app config never suppresses a migrated component', async () => { // Exact, so re-running --suppress-rule cannot quietly absorb new antd usage. // Lower this when a PR removes antd imports and prunes the baseline. - const EXPECTED_SUPPRESSED_ANTD_SYMBOLS = 148; + const EXPECTED_SUPPRESSED_ANTD_SYMBOLS = 113; const suppressedSymbols = Object.values(suppressions).reduce( (total, rules) => total + (rules['openmetadata-antd/no-antd-import']?.count ?? 0), diff --git a/openmetadata-ui/src/main/resources/ui/eslint-suppressions.json b/openmetadata-ui/src/main/resources/ui/eslint-suppressions.json index 773275daa056..a2ae9ad8bff8 100644 --- a/openmetadata-ui/src/main/resources/ui/eslint-suppressions.json +++ b/openmetadata-ui/src/main/resources/ui/eslint-suppressions.json @@ -727,7 +727,7 @@ }, "playwright/utils/importUtils.ts": { "om-playwright/no-positional-locator": { - "count": 21 + "count": 20 } }, "playwright/utils/incidentManager.ts": { @@ -803,11 +803,6 @@ "count": 3 } }, - "src/components/ContextCenter/CreateMemoryModal/CreateMemoryModal.component.tsx": { - "openmetadata-antd/no-antd-import": { - "count": 1 - } - }, "src/components/Dashboard/DashboardVersion/DashboardVersion.component.tsx": { "openmetadata-antd/no-antd-import": { "count": 1 @@ -833,14 +828,9 @@ "count": 2 } }, - "src/components/DataInsight/TotalEntityInsightSummary.component.tsx": { - "openmetadata-antd/no-antd-import": { - "count": 1 - } - }, "src/components/Database/TableQueries/TableQueries.tsx": { "openmetadata-antd/no-antd-import": { - "count": 3 + "count": 2 } }, "src/components/Database/TableQueries/TableQueryRightPanel/TableQueryRightPanel.component.tsx": { @@ -918,11 +908,6 @@ "count": 1 } }, - "src/components/Pipeline/Execution/Execution.component.tsx": { - "openmetadata-antd/no-antd-import": { - "count": 2 - } - }, "src/components/Pipeline/PipelineTaskTab/PipelineTaskTab.tsx": { "openmetadata-antd/no-antd-import": { "count": 1 @@ -978,11 +963,6 @@ "count": 1 } }, - "src/components/Settings/Bot/BotDetails/AuthMechanismForm.test.tsx": { - "openmetadata-antd/no-antd-import": { - "count": 1 - } - }, "src/components/Settings/Bot/BotDetails/BotDetails.component.tsx": { "openmetadata-antd/no-antd-import": { "count": 2 @@ -1013,41 +993,11 @@ "count": 1 } }, - "src/components/common/AsyncSelect/AsyncSelect.test.tsx": { - "openmetadata-antd/no-antd-import": { - "count": 1 - } - }, - "src/components/common/AsyncSelect/AsyncSelect.tsx": { - "openmetadata-antd/no-antd-import": { - "count": 3 - } - }, - "src/components/common/AsyncSelect/AsyncSelectList.interface.ts": { - "openmetadata-antd/no-antd-import": { - "count": 1 - } - }, - "src/components/common/DatePicker/DatePicker.tsx": { - "openmetadata-antd/no-antd-import": { - "count": 2 - } - }, "src/components/common/DraggableTabs/DraggableTabs.tsx": { "openmetadata-antd/no-antd-import": { "count": 4 } }, - "src/components/common/DrawerPopupContainerProvider/DrawerPopupContainerProvider.tsx": { - "openmetadata-antd/no-antd-import": { - "count": 1 - } - }, - "src/components/common/FilterPattern/FilterPattern.tsx": { - "openmetadata-antd/no-antd-import": { - "count": 1 - } - }, "src/components/common/Form/FormItemLabel.tsx": { "openmetadata-antd/no-antd-import": { "count": 1 @@ -1098,11 +1048,6 @@ "count": 1 } }, - "src/components/common/SanitizedInput/SanitizedInput.tsx": { - "openmetadata-antd/no-antd-import": { - "count": 2 - } - }, "src/components/common/Table/DraggableMenu/DraggableMenuItem.component.tsx": { "openmetadata-antd/no-antd-import": { "count": 2 @@ -1113,31 +1058,11 @@ "count": 1 } }, - "src/components/common/UserTeamSelectableListSearchInput/UserTeamSelectableListSearchInput.component.tsx": { - "openmetadata-antd/no-antd-import": { - "count": 1 - } - }, - "src/constants/Form.constants.ts": { - "openmetadata-antd/no-antd-import": { - "count": 1 - } - }, "src/context/AntDConfigProvider/AntDConfigProvider.tsx": { "openmetadata-antd/no-antd-import": { "count": 1 } }, - "src/interface/FormUtils.interface.ts": { - "openmetadata-antd/no-antd-import": { - "count": 3 - } - }, - "src/pages/AddNotificationPage/AddNotificationPage.tsx": { - "openmetadata-antd/no-antd-import": { - "count": 3 - } - }, "src/pages/AppearanceConfigSettingsPage/AppearanceConfigSettingsPage.tsx": { "openmetadata-antd/no-antd-import": { "count": 1 @@ -1208,21 +1133,6 @@ "count": 2 } }, - "src/utils/Alerts/AlertsUtilPure.ts": { - "openmetadata-antd/no-antd-import": { - "count": 1 - } - }, - "src/utils/CSV/CSVUtilsClassBase.tsx": { - "openmetadata-antd/no-antd-import": { - "count": 1 - } - }, - "src/utils/CSV/EntityBulkEditConfigClassBase.ts": { - "openmetadata-antd/no-antd-import": { - "count": 1 - } - }, "src/utils/DataAssetsVersionHeaderUtils.tsx": { "openmetadata-antd/no-antd-import": { "count": 1 @@ -1248,11 +1158,6 @@ "count": 1 } }, - "src/utils/FilterQueryUtils.ts": { - "openmetadata-antd/no-antd-import": { - "count": 1 - } - }, "src/utils/Lineage/LineageUtils.tsx": { "openmetadata-antd/no-antd-import": { "count": 2 @@ -1273,11 +1178,6 @@ "count": 1 } }, - "src/utils/SearchSettingsUtils.ts": { - "openmetadata-antd/no-antd-import": { - "count": 1 - } - }, "src/utils/TableColumn.util.tsx": { "openmetadata-antd/no-antd-import": { "count": 1 @@ -1298,11 +1198,6 @@ "count": 1 } }, - "src/utils/formUtils.tsx": { - "openmetadata-antd/no-antd-import": { - "count": 5 - } - }, "src/utils/mocks/EntitySummaryPanelUtils.mock.tsx": { "openmetadata-antd/no-antd-import": { "count": 1 diff --git a/openmetadata-ui/src/main/resources/ui/eslint.config.mjs b/openmetadata-ui/src/main/resources/ui/eslint.config.mjs index 627f7caa8c9f..eb8e96c2c514 100644 --- a/openmetadata-ui/src/main/resources/ui/eslint.config.mjs +++ b/openmetadata-ui/src/main/resources/ui/eslint.config.mjs @@ -37,6 +37,10 @@ import omPlaywright from './playwright/eslint-rules/index.mjs'; // here in the PR that removes its last import (open-metadata/openmetadata-collate#6884). const MIGRATED_ANTD_COMPONENTS = [ 'AutoComplete', + 'DatePicker', + 'Form', + 'Input', + 'Select', 'Checkbox', 'InputNumber', 'Radio', diff --git a/openmetadata-ui/src/main/resources/ui/playwright/eslint-rules/tests/corpus.test.mjs b/openmetadata-ui/src/main/resources/ui/playwright/eslint-rules/tests/corpus.test.mjs index ffd73e2a8532..5428acebc604 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/eslint-rules/tests/corpus.test.mjs +++ b/openmetadata-ui/src/main/resources/ui/playwright/eslint-rules/tests/corpus.test.mjs @@ -66,7 +66,8 @@ test('the suppressions baseline matches its recorded state exactly', () => { // Targeting the core TreeSelect popovers and team picker by test id in the // UserDetails and team-editor specs removes more, and pinning // customize-navigation switches to their tree rows removes four more. - 'om-playwright/no-positional-locator': 560, + // Picking the bulk-import entity type by its option role removes one more. + 'om-playwright/no-positional-locator': 559, 'om-playwright/require-assertion-per-test': 1, 'playwright/no-skipped-test': 1, 'playwright/no-wait-for-selector': 14, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotDetails/AuthMechanismForm.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotDetails/AuthMechanismForm.test.tsx index 3a04a19d8820..e58fc45e8e48 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotDetails/AuthMechanismForm.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotDetails/AuthMechanismForm.test.tsx @@ -12,7 +12,6 @@ */ import { render, screen, waitFor, within } from '@testing-library/react'; -import { Select } from 'antd'; import { PersonalAccessToken, TokenType, @@ -25,8 +24,6 @@ import { import { SettingType } from '../../../../generated/settings/settings'; import AuthMechanismForm from './AuthMechanismForm'; -const { Option } = Select; - const mockUpdateSettingsConfig = jest.fn(); const mockPatchSettingsConfig = jest.fn(); const mockShowErrorToast = jest.fn(); @@ -56,11 +53,7 @@ jest.mock('../../../../utils/BotsUtils', () => ({ ? options.filter((opt) => opt.value !== JWTTokenExpiry.Unlimited) : options; - return filteredOptions.map((opt) => ( - - )); + return filteredOptions.map((opt) => ({ id: opt.value, label: opt.label })); }), })); From f24d9af252e983c9d5dc200920234c12314a07d1 Mon Sep 17 00:00:00 2001 From: Harshit Shah Date: Thu, 15 Oct 2026 12:54:21 +0530 Subject: [PATCH 08/34] style(ui): organize imports in the migrated date-filter callers --- .../ui/src/components/Database/TableQueries/TableQueries.tsx | 2 +- .../main/resources/ui/src/pages/AuditLogsPage/AuditLogsPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueries.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueries.tsx index 874128dabc25..d90dcfc2ea00 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueries.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueries.tsx @@ -31,7 +31,6 @@ import Qs from 'qs'; import { FC, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import DqDateRangeFilter from '../../observability/DataQuality/Dashboard/DqDateRangeFilter'; import { WILD_CARD_CHAR } from '../../../constants/char.constants'; import { INITIAL_PAGING_VALUE, @@ -78,6 +77,7 @@ import FilterSelectDropdown from '../../common/FilterSelectDropdown/FilterSelect import Loader from '../../common/Loader/Loader'; import ResizablePanels from '../../common/ResizablePanels/ResizablePanels'; import SortingDropDown from '../../Explore/SortingDropDown'; +import DqDateRangeFilter from '../../observability/DataQuality/Dashboard/DqDateRangeFilter'; import PaginationComponent from '../../PaginationComponent/PaginationComponent'; import { SearchDropdownOption } from '../../SearchDropdown/SearchDropdown.interface'; import QueryCard from './QueryCard'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AuditLogsPage/AuditLogsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AuditLogsPage/AuditLogsPage.tsx index e6e1b9794fcb..5364581a1ec0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AuditLogsPage/AuditLogsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AuditLogsPage/AuditLogsPage.tsx @@ -63,11 +63,11 @@ import { AuditLogListResponse, } from '../../types/auditLogs.interface'; import { buildParamsFromFilters } from '../../utils/AuditLogUtils'; -import { CUSTOM_DATE_RANGE_KEY } from '../../utils/DatePickerMenuUtils'; import { dateValueToMillis, millisToDateValue, } from '../../utils/date-time/calendarDate.utils'; +import { CUSTOM_DATE_RANGE_KEY } from '../../utils/DatePickerMenuUtils'; import { getSettingPath } from '../../utils/RouterUtils'; import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; import './AuditLogsPage.less'; From 6a2d8b7d90905ab0e69ebee43d2c526ca6d62366 Mon Sep 17 00:00:00 2001 From: Harshit Shah Date: Thu, 15 Oct 2026 12:56:31 +0530 Subject: [PATCH 09/34] test(ui): guard the metric enum editor before rendering it --- .../resources/ui/src/utils/CSV/CSVUtilsClassBase.test.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.test.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.test.tsx index 8152a35f2071..d976f80f8710 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.test.tsx @@ -478,6 +478,10 @@ describe('CSV utils ClassBase', () => { const onRowChange = jest.fn(); const onClose = jest.fn(); + if (!editor) { + throw new Error('Expected metricType editor to be defined'); + } + render( <> {editor({ From 772933df4e7685b8d0ba22471533d1b32c08e969 Mon Sep 17 00:00:00 2001 From: Harshit Shah Date: Thu, 15 Oct 2026 13:09:12 +0530 Subject: [PATCH 10/34] fix(ui): draw the navbar search as one field again Moving the global search off antd (#35280) swapped its borderless antd Input and Select for core ones that draw their own surface and outline, so the bar showed three boxes inside its container. Core Input and Select gain a borderless option (Select keeps its keyboard focus outline) and the search bar uses it. --- .../src/components/base/input/input.test.tsx | 15 ++++++++ .../ui/src/components/base/input/input.tsx | 10 ++++++ .../components/base/select/select.test.tsx | 15 ++++++++ .../ui/src/components/base/select/select.tsx | 34 +++++++++++++------ .../GlobalSearchBar/GlobalSearchBar.tsx | 2 ++ 5 files changed, 66 insertions(+), 10 deletions(-) diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/input/input.test.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/input/input.test.tsx index 47f0065a1fa2..9dbbefcb0990 100644 --- a/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/input/input.test.tsx +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/input/input.test.tsx @@ -17,6 +17,21 @@ import { Input } from './input'; import { PasswordInput } from './password-input'; describe('Input', () => { + it('drops its own border when borderless', () => { + const { rerender } = render(); + // The border is drawn on the group that wraps the input. + const group = () => + screen + .getByRole('textbox', { name: /Search/ }) + .closest('[role="presentation"]'); + + expect(group()).not.toHaveClass('tw:outline-0'); + + rerender(); + + expect(group()).toHaveClass('tw:outline-0', 'tw:bg-transparent'); + }); + it('greys the input text when the field is disabled', () => { render(); diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/input/input.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/input/input.tsx index f47f49509aa8..a2e02e9b8e7b 100644 --- a/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/input/input.tsx +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/input/input.tsx @@ -62,6 +62,11 @@ export interface InputBaseProps extends TextFieldProps { trailingSlot?: ReactNode; /** Passes `data-testid` directly to the inner `` element (not the wrapper). */ inputDataTestId?: string; + /** + * Drop the field's own surface, border and focus outline, for an input that + * sits inside a container which already draws them (e.g. a search bar). + */ + borderless?: boolean; } const TextFieldContext = createContext({}); @@ -78,6 +83,7 @@ export const InputBase = forwardRef< groupRef, trailingSlot, inputDataTestId, + borderless, size = 'sm', fontSize = 'sm', isInvalid, @@ -162,6 +168,8 @@ export const InputBase = forwardRef< isFocusWithin && 'tw:group-invalid:outline-2 tw:group-invalid:-outline-offset-2 tw:group-invalid:outline-error', + borderless && 'tw:bg-transparent tw:shadow-none tw:outline-0', + context?.wrapperClassName, wrapperClassName ) @@ -314,6 +322,7 @@ export const Input = forwardRef>( shortcut, trailingSlot, inputDataTestId, + borderless, hideRequiredIndicator, className, groupRef, @@ -361,6 +370,7 @@ export const Input = forwardRef>( shortcut, trailingSlot, inputDataTestId, + borderless, iconClassName, inputClassName, wrapperClassName, diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/select/select.test.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/select/select.test.tsx index 83c04f692d22..587623012677 100644 --- a/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/select/select.test.tsx +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/select/select.test.tsx @@ -411,3 +411,18 @@ describe('Select with a large collection', () => { }); }); }); + +describe('Select borderless', () => { + it('drops the trigger border until keyboard focus', () => { + render( + + ); + + expect(screen.getByRole('button', { name: /Index/ })).toHaveClass( + 'tw:outline-0', + 'tw:bg-transparent' + ); + }); +}); diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/select/select.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/select/select.tsx index 4928b615611a..a6bd8b59fd03 100644 --- a/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/select/select.tsx +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/select/select.tsx @@ -62,6 +62,11 @@ interface SelectProps items?: SelectItemType[]; popoverClassName?: string; icon?: FC | ReactNode; + /** + * Drop the trigger's own surface and border (keyboard focus still shows), + * for a select inside a container that already draws them (e.g. a search bar). + */ + borderless?: boolean; children: ReactNode | ((item: SelectItemType) => ReactNode); } @@ -73,6 +78,7 @@ interface SelectValueProps { isDisabled: boolean; placeholder?: string; icon?: FC | ReactNode; + borderless?: boolean; } export const sizes = { @@ -88,19 +94,25 @@ const SelectValue = ({ fontSize, placeholder, icon, + borderless, }: SelectValueProps) => { return ( + className={({ isFocusVisible }) => + cx( + // Border drawn with outline, not a ring: WebKit does not pixel-snap box-shadow, so + // a ring thins/vanishes in Safari when zoomed out. `outline-hidden` is gone — the + // outline IS the border and focus indicator here, as in input.tsx. + 'tw:relative tw:flex tw:w-full tw:cursor-pointer tw:items-center tw:rounded-lg tw:bg-primary tw:shadow-xs tw:outline-1 tw:-outline-offset-1 tw:outline-primary tw:transition tw:duration-100 tw:ease-linear', + (isFocused || isOpen) && + 'tw:outline-2 tw:-outline-offset-2 tw:outline-brand', + isDisabled && + 'tw:cursor-not-allowed tw:bg-disabled_subtle tw:text-disabled', + borderless && + !isFocusVisible && + 'tw:bg-transparent tw:shadow-none tw:outline-0' + ) + }> className={cx( 'tw:flex tw:h-max tw:w-full tw:items-center tw:justify-start tw:gap-2 tw:truncate tw:text-left tw:align-middle', @@ -194,6 +206,7 @@ const Select = forwardRef>( tooltip, emptyState, className, + borderless, // Without this react-aria keeps an empty list closed, so `emptyState` // would never render. allowsEmptyCollection = true, @@ -227,6 +240,7 @@ const Select = forwardRef>( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/GlobalSearchBar/GlobalSearchBar.tsx b/openmetadata-ui/src/main/resources/ui/src/components/GlobalSearchBar/GlobalSearchBar.tsx index 72f0229ea242..83723272cac8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/GlobalSearchBar/GlobalSearchBar.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/GlobalSearchBar/GlobalSearchBar.tsx @@ -102,6 +102,7 @@ export const GlobalSearchBar = () => { const entitiesSelect = useMemo( () => ( Date: Thu, 15 Oct 2026 08:01:18 +0000 Subject: [PATCH 11/34] chore(playwright): auto-refresh impact-map.generated.json --- .github/playwright/impact-map.generated.json | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/playwright/impact-map.generated.json b/.github/playwright/impact-map.generated.json index 41693e7185c1..d586fa6d8baa 100644 --- a/.github/playwright/impact-map.generated.json +++ b/.github/playwright/impact-map.generated.json @@ -3885,7 +3885,6 @@ "playwright/e2e/Features/DataQuality/TestCaseDetailsPage.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseIncidentPermissions.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseResultPermissions.spec.ts", - "playwright/e2e/Pages/AuditLogs.spec.ts", "playwright/e2e/Pages/DataContractsSemanticRules.spec.ts", "playwright/e2e/Pages/DataInsightSettings.spec.ts", "playwright/e2e/Pages/Entity.spec.ts", From 8fae8119d1f69bb7bfcaf50ec5bc5aaed0afe671 Mon Sep 17 00:00:00 2001 From: Harshit Shah Date: Thu, 15 Oct 2026 13:49:29 +0530 Subject: [PATCH 12/34] refactor(ui): replace AntDConfigProvider with a theme CSS-variable hook The provider's only live job was writing the custom theme into the --ant-* CSS variables our LESS reads. useThemeCssVariables now writes the configured primary and status colours (only when set, as ConfigProvider.config did) next to the existing palette, so OSS has no antd import left. ConfigProvider joins MIGRATED_ANTD_COMPONENTS; antd symbol suppressions 113 -> 112, all @ant-design/icons. RetentionPeriod's alert drops its antd-tint overrides for the core brand style. --- .../eslint-rules/openmetadata-antd.test.mjs | 2 +- .../resources/ui/eslint-suppressions.json | 5 -- .../src/main/resources/ui/eslint.config.mjs | 1 + .../src/main/resources/ui/src/AppRoot.tsx | 21 +++---- .../RetentionPeriod.component.tsx | 9 +-- .../ui/src/hooks/useThemeCssVariables.test.ts | 61 ++++++++++++++++++ .../useThemeCssVariables.ts} | 62 ++++++++++--------- .../src/main/resources/ui/src/styles/app.less | 4 +- 8 files changed, 110 insertions(+), 55 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/hooks/useThemeCssVariables.test.ts rename openmetadata-ui/src/main/resources/ui/src/{context/AntDConfigProvider/AntDConfigProvider.tsx => hooks/useThemeCssVariables.ts} (55%) diff --git a/openmetadata-ui/src/main/resources/ui/eslint-rules/openmetadata-antd.test.mjs b/openmetadata-ui/src/main/resources/ui/eslint-rules/openmetadata-antd.test.mjs index 96b6f007184b..6bd7793497c3 100644 --- a/openmetadata-ui/src/main/resources/ui/eslint-rules/openmetadata-antd.test.mjs +++ b/openmetadata-ui/src/main/resources/ui/eslint-rules/openmetadata-antd.test.mjs @@ -163,7 +163,7 @@ test('the app config never suppresses a migrated component', async () => { // Exact, so re-running --suppress-rule cannot quietly absorb new antd usage. // Lower this when a PR removes antd imports and prunes the baseline. - const EXPECTED_SUPPRESSED_ANTD_SYMBOLS = 113; + const EXPECTED_SUPPRESSED_ANTD_SYMBOLS = 112; const suppressedSymbols = Object.values(suppressions).reduce( (total, rules) => total + (rules['openmetadata-antd/no-antd-import']?.count ?? 0), diff --git a/openmetadata-ui/src/main/resources/ui/eslint-suppressions.json b/openmetadata-ui/src/main/resources/ui/eslint-suppressions.json index a2ae9ad8bff8..93c47eee9684 100644 --- a/openmetadata-ui/src/main/resources/ui/eslint-suppressions.json +++ b/openmetadata-ui/src/main/resources/ui/eslint-suppressions.json @@ -1058,11 +1058,6 @@ "count": 1 } }, - "src/context/AntDConfigProvider/AntDConfigProvider.tsx": { - "openmetadata-antd/no-antd-import": { - "count": 1 - } - }, "src/pages/AppearanceConfigSettingsPage/AppearanceConfigSettingsPage.tsx": { "openmetadata-antd/no-antd-import": { "count": 1 diff --git a/openmetadata-ui/src/main/resources/ui/eslint.config.mjs b/openmetadata-ui/src/main/resources/ui/eslint.config.mjs index eb8e96c2c514..8636467094ad 100644 --- a/openmetadata-ui/src/main/resources/ui/eslint.config.mjs +++ b/openmetadata-ui/src/main/resources/ui/eslint.config.mjs @@ -53,6 +53,7 @@ const MIGRATED_ANTD_COMPONENTS = [ 'Badge', 'Carousel', 'Collapse', + 'ConfigProvider', 'Drawer', 'Layout', 'List', diff --git a/openmetadata-ui/src/main/resources/ui/src/AppRoot.tsx b/openmetadata-ui/src/main/resources/ui/src/AppRoot.tsx index 7e0557166cfe..39dedc068896 100644 --- a/openmetadata-ui/src/main/resources/ui/src/AppRoot.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/AppRoot.tsx @@ -21,8 +21,8 @@ import { useShallow } from 'zustand/react/shallow'; import App from './App'; import { DocumentTitleProvider } from './components/common/DocumentTitle/DocumentTitleProvider'; import ErrorBoundary from './components/common/ErrorBoundary/ErrorBoundary'; -import AntDConfigProvider from './context/AntDConfigProvider/AntDConfigProvider'; import { useApplicationStore } from './hooks/useApplicationStore'; +import { useThemeCssVariables } from './hooks/useThemeCssVariables'; import { getCustomUiThemePreference, getSystemConfig, @@ -34,6 +34,7 @@ import { getThemeConfig } from './utils/ThemeUtils'; const AppRoot: FC = () => { const { initializeAuthState } = useApplicationStore(); + useThemeCssVariables(); const { applicationConfig, setApplicationConfig, setRdfEnabled } = useApplicationStore( @@ -91,16 +92,14 @@ const AppRoot: FC = () => { basename={getBasePath()} useTransitions={!isPlaywrightEnv()}> - - - - - - - - - - + + + + + + + +
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/RetentionPeriod/RetentionPeriod.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/RetentionPeriod/RetentionPeriod.component.tsx index 2f3f566cd74d..68b20262ed31 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/RetentionPeriod/RetentionPeriod.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/RetentionPeriod/RetentionPeriod.component.tsx @@ -152,14 +152,7 @@ const RetentionPeriod = ({ id="retention-period-form" onSubmit={handleSubmit}> - + {t('message.retention-period-description')} diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useThemeCssVariables.test.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useThemeCssVariables.test.ts new file mode 100644 index 000000000000..dec91c727467 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useThemeCssVariables.test.ts @@ -0,0 +1,61 @@ +/* + * 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 { renderHook } from '@testing-library/react'; +import { useApplicationStore } from './useApplicationStore'; +import { useThemeCssVariables } from './useThemeCssVariables'; + +jest.mock('./useApplicationStore', () => ({ + useApplicationStore: jest.fn(), +})); + +const mockTheme = (customTheme: Record | undefined) => + (useApplicationStore as unknown as jest.Mock).mockImplementation((selector) => + selector({ applicationConfig: { customTheme } }) + ); + +const cssVar = (name: string) => + document.documentElement.style.getPropertyValue(name); + +describe('useThemeCssVariables', () => { + beforeEach(() => { + document.documentElement.removeAttribute('style'); + }); + + it('writes the configured brand and status colours', () => { + mockTheme({ + primaryColor: '#123456', + successColor: '#00aa00', + warningColor: '#aa8800', + errorColor: '#aa0000', + infoColor: '#0088aa', + }); + + renderHook(() => useThemeCssVariables()); + + expect(cssVar('--ant-primary-color')).toBe('#123456'); + expect(cssVar('--ant-success-color')).toBe('#00aa00'); + expect(cssVar('--ant-warning-color')).toBe('#aa8800'); + expect(cssVar('--ant-error-color')).toBe('#aa0000'); + expect(cssVar('--ant-info-color')).toBe('#0088aa'); + expect(cssVar('--ant-primary-7')).not.toBe(''); + }); + + it('leaves unconfigured status colours to the stylesheet defaults', () => { + mockTheme({ primaryColor: '#123456' }); + + renderHook(() => useThemeCssVariables()); + + expect(cssVar('--ant-primary-color')).toBe('#123456'); + expect(cssVar('--ant-success-color')).toBe(''); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/context/AntDConfigProvider/AntDConfigProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/hooks/useThemeCssVariables.ts similarity index 55% rename from openmetadata-ui/src/main/resources/ui/src/context/AntDConfigProvider/AntDConfigProvider.tsx rename to openmetadata-ui/src/main/resources/ui/src/hooks/useThemeCssVariables.ts index b976ac2644a1..a2de26332b85 100644 --- a/openmetadata-ui/src/main/resources/ui/src/context/AntDConfigProvider/AntDConfigProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useThemeCssVariables.ts @@ -10,32 +10,48 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ConfigProvider } from 'antd'; -import { FC, ReactNode, useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import { DEFAULT_THEME } from '../../constants/Appearance.constants'; -import { useApplicationStore } from '../../hooks/useApplicationStore'; -import { generatePalette } from '../../styles/colorPallet'; +import { useEffect } from 'react'; +import { DEFAULT_THEME } from '../constants/Appearance.constants'; +import { useApplicationStore } from './useApplicationStore'; +import { generatePalette } from '../styles/colorPallet'; -const AntDConfigProvider: FC<{ children: ReactNode }> = ({ children }) => { - const { i18n } = useTranslation(); +// The LESS reads the brand and status colours through these variables +// (`@primary-color: var(--ant-primary-color)`, `@success-color`, ...). The +// antd stylesheet still supplies their defaults; only the configured ones are +// written, as antd's ConfigProvider.config did. +const THEME_COLOR_VARIABLES = { + primaryColor: '--ant-primary-color', + successColor: '--ant-success-color', + warningColor: '--ant-warning-color', + errorColor: '--ant-error-color', + infoColor: '--ant-info-color', +} as const; + +/** Writes the configured theme into the app-wide CSS variables. */ +export const useThemeCssVariables = () => { const applicationConfig = useApplicationStore( (state) => state.applicationConfig ); + const customTheme = applicationConfig?.customTheme; + + useEffect(() => { + Object.entries(THEME_COLOR_VARIABLES).forEach(([key, variable]) => { + const color = customTheme?.[key as keyof typeof THEME_COLOR_VARIABLES]; + if (color) { + document.documentElement.style.setProperty(variable, color); + } + }); + }, [customTheme]); useEffect(() => { const palette = generatePalette( - applicationConfig?.customTheme?.primaryColor ?? DEFAULT_THEME.primaryColor + customTheme?.primaryColor ?? DEFAULT_THEME.primaryColor ); // Use custom hover color if provided, otherwise use generated palette[1] const hoverColor = - applicationConfig?.customTheme?.hoverColor || - palette[2] || - DEFAULT_THEME.hoverColor; + customTheme?.hoverColor || palette[2] || DEFAULT_THEME.hoverColor; const selectedColor = - applicationConfig?.customTheme?.selectedColor || - palette[8] || - DEFAULT_THEME.hoverColor; + customTheme?.selectedColor || palette[8] || DEFAULT_THEME.hoverColor; palette.forEach((color, index) => { switch (index) { case 0: @@ -77,18 +93,8 @@ const AntDConfigProvider: FC<{ children: ReactNode }> = ({ children }) => { selectedColor ); }, [ - applicationConfig?.customTheme?.primaryColor, - applicationConfig?.customTheme?.hoverColor, - applicationConfig?.customTheme?.selectedColor, + customTheme?.primaryColor, + customTheme?.hoverColor, + customTheme?.selectedColor, ]); - - ConfigProvider.config({ - theme: { - ...applicationConfig?.customTheme, - }, - }); - - return {children}; }; - -export default AntDConfigProvider; diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/app.less b/openmetadata-ui/src/main/resources/ui/src/styles/app.less index 503d0dffb253..3b95be9ec0ca 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/app.less +++ b/openmetadata-ui/src/main/resources/ui/src/styles/app.less @@ -871,7 +871,7 @@ a[href].link-text-grey, // AI dark-mode brand ACCENTS -> blue-400 (#53b1fd), the devrel dark palette: // #1570ef carries fills/borders, #53b1fd labels/icons/links (~5.4:1 on dark; // #1570ef is only ~3.3:1 as text). -// UntitledUIThemeProvider + AntDConfigProvider inject the configured brand +// UntitledUIThemeProvider + useThemeCssVariables inject the configured brand // primary (#1570ef) as theme-agnostic inline custom properties on — the // same element that carries `.dark-mode` — so the token layer's dark flip can't // override them (inline wins on that element). Re-point the brand *accent* vars @@ -906,7 +906,7 @@ a[href].link-text-grey, } // Legacy Ant links: `.text-link-color` uses `@primary-color` -// (= var(--ant-primary-color)), injected by AntDConfigProvider as #1570ef and +// (= var(--ant-primary-color)), injected by useThemeCssVariables as #1570ef and // never flipped. Point it at the core link token, which flips in dark. .dark-mode .text-link-color, .dark-mode a.text-link-color { From e32c0a34dce7cec1e00305283ad0156aa16f6127 Mon Sep 17 00:00:00 2001 From: Harshit Shah Date: Thu, 15 Oct 2026 13:58:37 +0530 Subject: [PATCH 13/34] chore(ui): drop antd and its rc-* stack from the bootstrap allow-list With AntDConfigProvider gone, antd, @ant-design/icons and their rc-*, async-validator and dom-align dependencies no longer load on first view. The bundle budget fails until they leave the allow-list, so they cannot creep back into the bootstrap unnoticed. --- .../ui/scripts/bundle-boot-packages.json | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/scripts/bundle-boot-packages.json b/openmetadata-ui/src/main/resources/ui/scripts/bundle-boot-packages.json index 1ba8012b32eb..b51920602352 100644 --- a/openmetadata-ui/src/main/resources/ui/scripts/bundle-boot-packages.json +++ b/openmetadata-ui/src/main/resources/ui/scripts/bundle-boot-packages.json @@ -4,11 +4,7 @@ "@analytics/session-storage-utils", "@analytics/session-utils", "@analytics/type-utils", - "@ant-design/colors", - "@ant-design/icons", - "@ant-design/icons-svg", "@babel/runtime", - "@ctrl/tinycolor", "@fontsource/source-code-pro", "@internationalized/date", "@internationalized/number", @@ -18,18 +14,14 @@ "@tanstack/react-query", "@untitledui/file-icons", "analytics-utils", - "antd", - "async-validator", "axios", "call-bind-apply-helpers", "call-bound", "classnames", "clsx", - "compute-scroll-into-view", "cookie", "cookie-storage", "dlv", - "dom-align", "dunder-proto", "es-define-property", "es-errors", @@ -51,21 +43,6 @@ "object-inspect", "prop-types", "qs", - "rc-align", - "rc-field-form", - "rc-input", - "rc-motion", - "rc-notification", - "rc-overflow", - "rc-pagination", - "rc-picker", - "rc-resize-observer", - "rc-select", - "rc-textarea", - "rc-tooltip", - "rc-trigger", - "rc-util", - "rc-virtual-list", "react", "react-aria", "react-aria-components", @@ -75,13 +52,10 @@ "react-helmet-async", "react-hook-form", "react-i18next", - "react-is", "react-router", "react-router-dom", "react-stately", - "resize-observer-polyfill", "scheduler", - "scroll-into-view-if-needed", "set-cookie-parser", "shallowequal", "side-channel", From 63690279a2d2c7a645c60e3a51b94430a2b39e33 Mon Sep 17 00:00:00 2001 From: Harshit Shah Date: Thu, 15 Oct 2026 14:18:33 +0530 Subject: [PATCH 14/34] fix(ui): drop theme CSS variables a saved theme clears The hook only wrote configured colours, so clearing a status colour in the theme settings left the old inline --ant-* variable on until reload; antd's ConfigProvider rebuilt its whole style tag instead. Remove the variable when the colour is empty, and sort the hook's imports for organize-imports. --- .../ui/src/hooks/useThemeCssVariables.test.ts | 12 ++++++++++++ .../resources/ui/src/hooks/useThemeCssVariables.ts | 5 ++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useThemeCssVariables.test.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useThemeCssVariables.test.ts index dec91c727467..d0678804f96a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/useThemeCssVariables.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useThemeCssVariables.test.ts @@ -58,4 +58,16 @@ describe('useThemeCssVariables', () => { expect(cssVar('--ant-primary-color')).toBe('#123456'); expect(cssVar('--ant-success-color')).toBe(''); }); + + it('drops a colour the theme no longer sets', () => { + mockTheme({ primaryColor: '#123456', successColor: '#00aa00' }); + const { rerender } = renderHook(() => useThemeCssVariables()); + + expect(cssVar('--ant-success-color')).toBe('#00aa00'); + + mockTheme({ primaryColor: '#123456', successColor: '' }); + rerender(); + + expect(cssVar('--ant-success-color')).toBe(''); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useThemeCssVariables.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useThemeCssVariables.ts index a2de26332b85..14f73bb7f609 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/useThemeCssVariables.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useThemeCssVariables.ts @@ -12,8 +12,8 @@ */ import { useEffect } from 'react'; import { DEFAULT_THEME } from '../constants/Appearance.constants'; -import { useApplicationStore } from './useApplicationStore'; import { generatePalette } from '../styles/colorPallet'; +import { useApplicationStore } from './useApplicationStore'; // The LESS reads the brand and status colours through these variables // (`@primary-color: var(--ant-primary-color)`, `@success-color`, ...). The @@ -39,6 +39,9 @@ export const useThemeCssVariables = () => { const color = customTheme?.[key as keyof typeof THEME_COLOR_VARIABLES]; if (color) { document.documentElement.style.setProperty(variable, color); + } else { + // A cleared colour falls back to the stylesheet default again. + document.documentElement.style.removeProperty(variable); } }); }, [customTheme]); From 01946ff547c147b34dc900dcc2f326add7bffdfc Mon Sep 17 00:00:00 2001 From: Harshit Shah Date: Thu, 15 Oct 2026 14:43:07 +0530 Subject: [PATCH 15/34] refactor(ui): share the date-range filter and picker helpers; tighten grid cells - Move DqDateRangeFilter to components/common/DateRangeFilter; table queries, pipeline executions and lineage no longer import a DQ-dashboard internal. An onClear prop replaces the clear button each caller composed. - calendarDate.utils owns the core-picker DateValue cast (core resolves its own @internationalized/date) and the end-of-day conversion; the filter, lineage time filter, audit-log export and KPI form use them, which also clears the filter's two type errors. - Grid enum cells keep an empty cell when the selection is cleared instead of writing "null"; multi-select and free-text cells no longer add duplicates. - Fix the invalid tw-max-w-md class on the system-alert placeholder. --- .../Profiler/TestSummary/TestSummary.test.tsx | 24 +++---- .../Profiler/TestSummary/TestSummary.tsx | 4 +- .../Database/TableQueries/TableQueries.tsx | 16 +---- .../LineageTimeFilter.component.tsx | 21 +++---- .../IncidentManager.component.tsx | 4 +- .../Execution/Execution.component.tsx | 24 ++----- .../DateRangeFilter.integration.test.tsx} | 10 +-- .../DateRangeFilter/DateRangeFilter.test.tsx} | 58 +++++++++++------ .../DateRangeFilter/DateRangeFilter.tsx} | 62 ++++++++++++++----- .../Dashboard/DqFilterBar.test.tsx | 2 +- .../DataQuality/Dashboard/DqFilterBar.tsx | 4 +- .../IncidentGroupsFilters.test.tsx | 2 +- .../IncidentGroups/IncidentGroupsFilters.tsx | 4 +- .../common/FilterChip/FilterChip.test.tsx | 2 +- .../common/FilterChip/FilterChip.tsx | 4 +- .../AddObservabilityPage.tsx | 2 +- .../src/pages/AuditLogsPage/AuditLogsPage.tsx | 12 ++-- .../ui/src/pages/KPIPage/KPIFormFields.tsx | 11 ++-- .../ui/src/utils/CSV/CSVUtilsClassBase.tsx | 14 +++-- .../date-time/calendarDate.utils.test.ts | 23 ++++++- .../src/utils/date-time/calendarDate.utils.ts | 23 +++++++ 21 files changed, 195 insertions(+), 131 deletions(-) rename openmetadata-ui/src/main/resources/ui/src/components/{observability/DataQuality/Dashboard/DqDateRangeFilter.integration.test.tsx => common/DateRangeFilter/DateRangeFilter.integration.test.tsx} (91%) rename openmetadata-ui/src/main/resources/ui/src/components/{observability/DataQuality/Dashboard/DqDateRangeFilter.test.tsx => common/DateRangeFilter/DateRangeFilter.test.tsx} (83%) rename openmetadata-ui/src/main/resources/ui/src/components/{observability/DataQuality/Dashboard/DqDateRangeFilter.tsx => common/DateRangeFilter/DateRangeFilter.tsx} (70%) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummary/TestSummary.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummary/TestSummary.test.tsx index c29429b15e16..c803704e49b0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummary/TestSummary.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummary/TestSummary.test.tsx @@ -90,11 +90,11 @@ interface DateRangeFilterProps { const mockDateRangeFilter = jest.fn(); jest.mock( - '../../../observability/DataQuality/Dashboard/DqDateRangeFilter', + '../../../common/DateRangeFilter/DateRangeFilter', () => (props: DateRangeFilterProps) => { mockDateRangeFilter(props); - return
DqDateRangeFilter
; + return
DateRangeFilter
; } ); @@ -140,7 +140,7 @@ describe('TestSummary component', () => { ).toBeInTheDocument(); expect(graphContainer).toBeInTheDocument(); expect(graph).toBeInTheDocument(); - expect(await screen.findByText('DqDateRangeFilter')).toBeInTheDocument(); + expect(await screen.findByText('DateRangeFilter')).toBeInTheDocument(); expect(screen.getByText('Result history')).toBeInTheDocument(); expect(screen.getByTestId('run-summary-tiles')).toBeInTheDocument(); }); @@ -265,7 +265,7 @@ describe('TestSummary component', () => { expect(screen.getByText('Loader.component')).toBeInTheDocument(); // Only the body waits: the header and its date picker stay, so nothing shifts. expect(screen.getByText('Result history')).toBeInTheDocument(); - expect(screen.getByText('DqDateRangeFilter')).toBeInTheDocument(); + expect(screen.getByText('DateRangeFilter')).toBeInTheDocument(); await waitFor(() => { expect(screen.getByText('TestSummaryGraph')).toBeInTheDocument(); @@ -275,7 +275,7 @@ describe('TestSummary component', () => { it('should open the range picker on the last 30 days', async () => { render(); - await screen.findByText('DqDateRangeFilter'); + await screen.findByText('DateRangeFilter'); expect(mockDateRangeFilter).toHaveBeenLastCalledWith( expect.objectContaining({ startTs: 1633948800000, endTs: 1633948800000 }) @@ -285,7 +285,7 @@ describe('TestSummary component', () => { it('should refetch exactly once when the range changes', async () => { render(); - await screen.findByText('DqDateRangeFilter'); + await screen.findByText('DateRangeFilter'); mockGetListTestCaseResults.mockClear(); const newDateRange = { startTs: 1234567890, endTs: 1234567899 }; @@ -304,7 +304,7 @@ describe('TestSummary component', () => { it('should not refetch when the same range is applied again', async () => { render(); - await screen.findByText('DqDateRangeFilter'); + await screen.findByText('DateRangeFilter'); mockGetListTestCaseResults.mockClear(); await applyRange({ startTs: 1633948800000, endTs: 1633948800000 }); @@ -318,7 +318,7 @@ describe('TestSummary component', () => { }); render(); - await screen.findByText('DqDateRangeFilter'); + await screen.findByText('DateRangeFilter'); expect(screen.getByTestId('run-summary-runs')).toHaveTextContent('1'); @@ -343,7 +343,7 @@ describe('TestSummary component', () => { it('should keep the newest range when an older response arrives late', async () => { render(); - await screen.findByText('DqDateRangeFilter'); + await screen.findByText('DateRangeFilter'); let resolveSlowResponse!: (value: unknown) => void; mockGetListTestCaseResults @@ -518,10 +518,10 @@ describe('TestSummary component', () => { testCaseResult: { timestamp: 1, testCaseStatus: 'Success' }, } as TestCase; const { rerender } = render(); - await screen.findByText('DqDateRangeFilter'); + await screen.findByText('DateRangeFilter'); rerender(); - await screen.findByText('DqDateRangeFilter'); + await screen.findByText('DateRangeFilter'); expect(mockGetListTestCaseResults).toHaveBeenCalledTimes(1); }); @@ -539,7 +539,7 @@ describe('TestSummary component', () => { 'This test has not run yet. Results will be plotted here after its first run.' ); expect(screen.getByText('Result history')).toBeInTheDocument(); - expect(screen.getByText('DqDateRangeFilter')).toBeInTheDocument(); + expect(screen.getByText('DateRangeFilter')).toBeInTheDocument(); expect(screen.queryByText('TestSummaryGraph')).not.toBeInTheDocument(); expect(screen.queryByTestId('run-summary-tiles')).not.toBeInTheDocument(); expect(screen.queryByTestId('run-details-card')).not.toBeInTheDocument(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummary/TestSummary.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummary/TestSummary.tsx index 97cc7a222fb1..a2b90e72b537 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummary/TestSummary.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummary/TestSummary.tsx @@ -39,9 +39,9 @@ import { formatDate } from '../../../../utils/date-time/DateTimeUtils'; import { translateWithNestedKeys } from '../../../../utils/i18next/LocalUtil'; import { showErrorToast } from '../../../../utils/ToastUtils'; import { useRequiredParams } from '../../../../utils/useRequiredParams'; +import DateRangeFilter from '../../../common/DateRangeFilter/DateRangeFilter'; import Loader from '../../../common/Loader/Loader'; import RunDetailsCard from '../../../DataQuality/IncidentManager/RunDetailsCard/RunDetailsCard'; -import DqDateRangeFilter from '../../../observability/DataQuality/Dashboard/DqDateRangeFilter'; import { TestSummaryProps } from '../ProfilerDashboard/profilerDashboard.interface'; import RunSummaryTiles from './RunSummaryTiles/RunSummaryTiles'; import './test-summary.less'; @@ -284,7 +284,7 @@ const TestSummary: React.FC = ({ data }) => { {caption}
- = ({ data-testid="data-range-picker" gap={2}> {t('label.created-date')} - onDateChange()} /> - {queryDateFilter && ( - } - onClick={() => onDateChange()} - /> - )}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityLineage/LineageTimeFilter.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityLineage/LineageTimeFilter.component.tsx index 66185772a3f0..d7c41e7fd2a4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityLineage/LineageTimeFilter.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityLineage/LineageTimeFilter.component.tsx @@ -23,23 +23,20 @@ import { useTranslation } from 'react-i18next'; import { ReactComponent as Calendar } from '../../../assets/svg/calendar.svg'; import { ReactComponent as DropdownIcon } from '../../../assets/svg/drop-down.svg'; import { - dateValueToMillis, - millisToDateValue, + dateValueToEndOfDayMillis, + millisToPickerDateValue, + PickerDateValue, } from '../../../utils/date-time/calendarDate.utils'; import { getCurrentMillis, getEpochMillisForPastDays, } from '../../../utils/date-time/DateTimeUtils'; -import DqDateRangeFilter from '../../observability/DataQuality/Dashboard/DqDateRangeFilter'; +import DateRangeFilter from '../../common/DateRangeFilter/DateRangeFilter'; import { LineageTimeFilterProps, LineageTimePresetKey, } from './LineageTimeFilter.interface'; -// Core resolves its own @internationalized/date copy, so its DateValue is a -// distinct nominal type from the app's; stage the picker value with core's own. -type CoreDateValue = Parameters[0]['value']; - const PRESET_DAYS: Record< Exclude< LineageTimePresetKey, @@ -62,7 +59,7 @@ const LineageTimeFilter: FC = ({ const { i18n, t } = useTranslation(); const [showCustomRange, setShowCustomRange] = useState(false); const [showPointInTime, setShowPointInTime] = useState(false); - const [pointValue, setPointValue] = useState(null); + const [pointValue, setPointValue] = useState(null); const matchedPresetDays = useMemo(() => { if (startTime === undefined || endTime === undefined) { @@ -126,9 +123,7 @@ const LineageTimeFilter: FC = ({ return; } if (key === LineageTimePresetKey.PointInTime) { - setPointValue( - (isPointInTime ? millisToDateValue(endTime) : null) as CoreDateValue - ); + setPointValue(isPointInTime ? millisToPickerDateValue(endTime) : null); setShowPointInTime(true); return; @@ -155,7 +150,7 @@ const LineageTimeFilter: FC = ({ // The date-only value resolves to midnight; include the whole day. onChange({ startTime: undefined, - endTime: dateValueToMillis(pointValue.add({ days: 1 })) - 1, + endTime: dateValueToEndOfDayMillis(pointValue), }); } }, [onChange, pointValue]); @@ -245,7 +240,7 @@ const LineageTimeFilter: FC = ({ {showCustomRange && ( - {t('label.date-range')} - { {view === PIPELINE_EXECUTION_TABS.LIST_VIEW ? ( - - + onDateChange()} /> - {datesSelected && ( - } - onClick={() => onDateChange()} - /> - )} - + ) : null} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/observability/DataQuality/Dashboard/DqDateRangeFilter.integration.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DateRangeFilter/DateRangeFilter.integration.test.tsx similarity index 91% rename from openmetadata-ui/src/main/resources/ui/src/components/observability/DataQuality/Dashboard/DqDateRangeFilter.integration.test.tsx rename to openmetadata-ui/src/main/resources/ui/src/components/common/DateRangeFilter/DateRangeFilter.integration.test.tsx index 52b605e4b67a..71ff1c7c91cf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/observability/DataQuality/Dashboard/DqDateRangeFilter.integration.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DateRangeFilter/DateRangeFilter.integration.test.tsx @@ -11,7 +11,7 @@ * limitations under the License. */ import { fireEvent, render, screen } from '@testing-library/react'; -import DqDateRangeFilter from './DqDateRangeFilter'; +import DateRangeFilter from './DateRangeFilter'; const START_TS = new Date('2024-01-15T00:00:00Z').getTime(); const END_TS = new Date('2024-01-20T00:00:00Z').getTime(); @@ -24,11 +24,11 @@ const END_TS = new Date('2024-01-20T00:00:00Z').getTime(); * production callers (`DqFilterBar`, `IncidentGroupsFilters`, `TestSummary`) * mount it. */ -describe('DqDateRangeFilter integration', () => { +describe('DateRangeFilter integration', () => { it('uncontrolled: reverts abandoned selection to the committed range after Escape close', () => { const onApply = jest.fn(); render( - + ); const trigger = screen.getByRole('button', { name: /date range/i }); @@ -57,7 +57,7 @@ describe('DqDateRangeFilter integration', () => { it('uncontrolled: reverts abandoned selection to the committed range after Dismiss close', () => { const onApply = jest.fn(); render( - + ); const trigger = screen.getByRole('button', { name: /date range/i }); @@ -84,7 +84,7 @@ describe('DqDateRangeFilter integration', () => { it('uncontrolled: Cancel then close reverts to the committed range', () => { const onApply = jest.fn(); render( - + ); const trigger = screen.getByRole('button', { name: /date range/i }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/observability/DataQuality/Dashboard/DqDateRangeFilter.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DateRangeFilter/DateRangeFilter.test.tsx similarity index 83% rename from openmetadata-ui/src/main/resources/ui/src/components/observability/DataQuality/Dashboard/DqDateRangeFilter.test.tsx rename to openmetadata-ui/src/main/resources/ui/src/components/common/DateRangeFilter/DateRangeFilter.test.tsx index f7ba871e86c2..2b2073939ce2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/observability/DataQuality/Dashboard/DqDateRangeFilter.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DateRangeFilter/DateRangeFilter.test.tsx @@ -13,7 +13,7 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { ComponentProps } from 'react'; -import DqDateRangeFilter from './DqDateRangeFilter'; +import DateRangeFilter from './DateRangeFilter'; type MockDateRangePickerProps = Pick< ComponentProps< @@ -33,6 +33,7 @@ type MockDateRangePickerProps = Pick< // and open/close triggers so we can assert the epoch-millis conversion and the // reset-on-close behaviour done by the component. jest.mock('@openmetadata/ui-core-components', () => ({ + ...jest.requireActual('@openmetadata/ui-core-components'), DateRangePicker: ({ value, onApply, @@ -72,7 +73,7 @@ jest.mock('@openmetadata/ui-core-components', () => ({ const START_TS = new Date('2024-01-15T00:00:00Z').getTime(); const END_TS = new Date('2024-01-20T00:00:00Z').getTime(); -describe('DqDateRangeFilter', () => { +describe('DateRangeFilter', () => { const onApply = jest.fn(); beforeEach(() => { @@ -80,26 +81,26 @@ describe('DqDateRangeFilter', () => { }); it('should render the date range picker', () => { - render(); + render(); expect(screen.getByTestId('date-range-picker')).toBeInTheDocument(); }); it('should render with null value when no timestamps are provided', () => { - render(); + render(); expect(screen.getByTestId('picker-value')).toHaveTextContent('null'); }); it('should render with null value when only start timestamp is provided', () => { - render(); + render(); expect(screen.getByTestId('picker-value')).toHaveTextContent('null'); }); it('should build a staged value when both timestamps are provided', () => { render( - + ); const value = screen.getByTestId('picker-value').textContent ?? ''; @@ -111,7 +112,7 @@ describe('DqDateRangeFilter', () => { it('should call onApply with epoch millis when applied with a valid range', () => { render( - + ); fireEvent.click(screen.getByTestId('picker-apply')); @@ -127,7 +128,7 @@ describe('DqDateRangeFilter', () => { it('should commit the end boundary as the last ms of the end day', () => { render( - + ); fireEvent.click(screen.getByTestId('picker-apply')); @@ -142,14 +143,14 @@ describe('DqDateRangeFilter', () => { }); it('should treat an epoch-0 timestamp as a real date, not absent', () => { - render(); + render(); // A truthiness check would drop startTs=0 and leave the staged value null. expect(screen.getByTestId('picker-value')).not.toHaveTextContent('null'); }); it('should not call onApply when there is no staged range', () => { - render(); + render(); fireEvent.click(screen.getByTestId('picker-apply')); @@ -158,7 +159,7 @@ describe('DqDateRangeFilter', () => { it('should not call onApply after clearing the staged range', () => { render( - + ); fireEvent.click(screen.getByTestId('picker-clear')); @@ -169,7 +170,7 @@ describe('DqDateRangeFilter', () => { it('should restore the committed range on cancel', () => { render( - + ); // Stage a clear, then cancel — value should be rebuilt from props. @@ -184,7 +185,7 @@ describe('DqDateRangeFilter', () => { it('should discard uncommitted selection on close (uncontrolled)', () => { render( - + ); const committedValue = screen.getByTestId('picker-value').textContent ?? ''; @@ -207,7 +208,7 @@ describe('DqDateRangeFilter', () => { it('should forward onOpenChange and discard on close (controlled)', () => { const onOpenChange = jest.fn(); render( - { it('should keep the [isOpen] force-close reset for the controlled path', () => { const { rerender } = render( - { // would NOT emit onOpenChange for this externally-driven close, so the effect // must still reset on its own. rerender( - { }); it('should leave the core trigger at its default size and width', () => { - render(); + render(); expect(screen.getByTestId('date-range-picker')).toHaveAttribute( 'data-full-width', @@ -284,7 +285,7 @@ describe('DqDateRangeFilter', () => { }); it('should pass the size and full width to the core trigger', () => { - render(); + render(); expect(screen.getByTestId('date-range-picker')).toHaveAttribute( 'data-full-width', @@ -295,4 +296,25 @@ describe('DqDateRangeFilter', () => { 'sm' ); }); + + it('offers a clear button only while a range is set, when onClear is given', () => { + const onClear = jest.fn(); + const { rerender } = render( + + ); + + expect(screen.queryByTestId('clear-date-filter')).not.toBeInTheDocument(); + + rerender( + + ); + fireEvent.click(screen.getByTestId('clear-date-filter')); + + expect(onClear).toHaveBeenCalledTimes(1); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/observability/DataQuality/Dashboard/DqDateRangeFilter.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DateRangeFilter/DateRangeFilter.tsx similarity index 70% rename from openmetadata-ui/src/main/resources/ui/src/components/observability/DataQuality/Dashboard/DqDateRangeFilter.tsx rename to openmetadata-ui/src/main/resources/ui/src/components/common/DateRangeFilter/DateRangeFilter.tsx index ce825188edf4..6d9b08971d2a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/observability/DataQuality/Dashboard/DqDateRangeFilter.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DateRangeFilter/DateRangeFilter.tsx @@ -10,17 +10,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { DateRangePicker } from '@openmetadata/ui-core-components'; +import { + Box, + ButtonUtility, + DateRangePicker, +} from '@openmetadata/ui-core-components'; +import { XClose } from '@openmetadata/ui-core-components/icons'; import { useEffect, useState } from 'react'; -import type { DateValue } from 'react-aria-components'; +import { useTranslation } from 'react-i18next'; import { + dateValueToEndOfDayMillis, dateValueToMillis, - millisToDateValue, -} from '../../../../utils/date-time/calendarDate.utils'; + millisToPickerDateValue, + PickerDateValue, +} from '../../../utils/date-time/calendarDate.utils'; -type PickerRange = { start: DateValue; end: DateValue } | null; +type PickerRange = { start: PickerDateValue; end: PickerDateValue } | null; -export interface DqDateRangeFilterProps { +export interface DateRangeFilterProps { startTs?: number; endTs?: number; /** `sm` matches the trigger to the sm filter inputs. */ @@ -32,6 +39,8 @@ export interface DqDateRangeFilterProps { onOpenChange?: (open: boolean) => void; /** Commit handler — compatible with the hook's `onDateRangeChange`. */ onApply: (range: { startTs: number; endTs: number }) => void; + /** Shows a clear button while a range is set; called when it is pressed. */ + onClear?: () => void; } /** @@ -39,7 +48,7 @@ export interface DqDateRangeFilterProps { * `@internationalized/date` values, to the dashboard's epoch-millis * `DateRangeObject`. Selection is staged locally and committed on Apply. */ -export const DqDateRangeFilter = ({ +export const DateRangeFilter = ({ startTs, endTs, size = 'md', @@ -47,10 +56,12 @@ export const DqDateRangeFilter = ({ isOpen, onOpenChange, onApply, -}: DqDateRangeFilterProps) => { + onClear, +}: DateRangeFilterProps) => { + const { t } = useTranslation(); const buildValue = (): PickerRange => { - const start = millisToDateValue(startTs); - const end = millisToDateValue(endTs); + const start = millisToPickerDateValue(startTs); + const end = millisToPickerDateValue(endTs); return start && end ? { start, end } : null; }; @@ -59,8 +70,8 @@ export const DqDateRangeFilter = ({ // Re-sync the staged value when the committed range changes upstream. useEffect(() => { - const start = millisToDateValue(startTs); - const end = millisToDateValue(endTs); + const start = millisToPickerDateValue(startTs); + const end = millisToPickerDateValue(endTs); setValue(start && end ? { start, end } : null); }, [startTs, endTs]); @@ -89,11 +100,9 @@ export const DqDateRangeFilter = ({ const handleApply = () => { if (value?.start && value?.end) { - // Commit the end boundary as the last millisecond of the selected day — - // the date-only value resolves to midnight, which would exclude the end day. onApply({ startTs: dateValueToMillis(value.start), - endTs: dateValueToMillis(value.end.add({ days: 1 })) - 1, + endTs: dateValueToEndOfDayMillis(value.end), }); } }; @@ -102,7 +111,7 @@ export const DqDateRangeFilter = ({ setValue(buildValue()); }; - return ( + const picker = ( ); + + if (!onClear) { + return picker; + } + + return ( + + {picker} + {startTs !== undefined && endTs !== undefined && ( + } + onClick={onClear} + /> + )} + + ); }; -export default DqDateRangeFilter; +export default DateRangeFilter; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/observability/DataQuality/Dashboard/DqFilterBar.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/observability/DataQuality/Dashboard/DqFilterBar.test.tsx index e001cb0a2b49..357c8d91e9d6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/observability/DataQuality/Dashboard/DqFilterBar.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/observability/DataQuality/Dashboard/DqFilterBar.test.tsx @@ -115,7 +115,7 @@ jest.mock( }) ); -jest.mock('./DqDateRangeFilter', () => ({ +jest.mock('../../../common/DateRangeFilter/DateRangeFilter', () => ({ __esModule: true, default: ({ onApply }: { onApply: (...args: unknown[]) => void }) => (
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/observability/DataQuality/Dashboard/DqFilterBar.tsx b/openmetadata-ui/src/main/resources/ui/src/components/observability/DataQuality/Dashboard/DqFilterBar.tsx index 1d59207b5974..ebcc2ec37597 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/observability/DataQuality/Dashboard/DqFilterBar.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/observability/DataQuality/Dashboard/DqFilterBar.tsx @@ -14,7 +14,7 @@ import { Box, Button } from '@openmetadata/ui-core-components'; import { XCircle } from '@openmetadata/ui-core-components/icons'; import { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import DqDateRangeFilter from './DqDateRangeFilter'; +import DateRangeFilter from '../../../common/DateRangeFilter/DateRangeFilter'; import { DqFilterBarProps } from './DqFilterBar.types'; import DqFilterChip from './DqFilterChip'; @@ -52,7 +52,7 @@ export const DqFilterBar = ({ return ( - ({ ), })); -jest.mock('../../DataQuality/Dashboard/DqDateRangeFilter', () => ({ +jest.mock('../../../common/DateRangeFilter/DateRangeFilter', () => ({ __esModule: true, default: jest .fn() diff --git a/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/IncidentGroupsFilters.tsx b/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/IncidentGroupsFilters.tsx index fc5c9d5ec882..11f31ed91e63 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/IncidentGroupsFilters.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/IncidentGroupsFilters.tsx @@ -38,9 +38,9 @@ import { getUserByName } from '../../../../rest/userAPI'; import { getEntityName } from '../../../../utils/EntityNameUtils'; import { getEntityReferenceFromEntity } from '../../../../utils/EntityReferenceUtils'; import { getNameFromFQN } from '../../../../utils/FqnUtils'; +import DateRangeFilter from '../../../common/DateRangeFilter/DateRangeFilter'; import { UserTeamSelectableList } from '../../../common/UserTeamSelectableList/UserTeamSelectableList.component'; import { useIncidentFilterOptions } from '../../../IncidentManager/useIncidentFilterOptions'; -import DqDateRangeFilter from '../../DataQuality/Dashboard/DqDateRangeFilter'; import { CLEARED_INCIDENT_GROUP_FILTERS, DEFAULT_INCIDENT_LIST_DATE_FIELD, @@ -294,7 +294,7 @@ const IncidentGroupsFilters = ({ /> - ({ Table: () => , })); -jest.mock('../../DataQuality/Dashboard/DqDateRangeFilter', () => ({ +jest.mock('../../../common/DateRangeFilter/DateRangeFilter', () => ({ __esModule: true, default: ({ onApply, startTs, endTs }: any) => ( + onPress={handleSelect} + /> ); } @@ -153,10 +154,11 @@ const LeftSidebar = () => { dataTestId={item.dataTestId} href={item.redirect_url} icon={item.icon} - size="sm" + size="lg" truncate={false} type={parentKey ? 'collapsible-child' : 'link'} - onClick={() => navigateToItem(item, parentKey)}> + variant="solid" + onClick={handleSelect}> {t(item.title)} ); @@ -234,6 +236,7 @@ const LeftSidebar = () => { return (
toggleGroup(item.key, event.currentTarget.open)}> { selectedKeys.includes(child.key) )} icon={item.icon} - size="sm" + size="lg" truncate={false} - type="collapsible"> + type="collapsible" + variant="solid"> {t(item.title)}
    {item.children?.map((child) => ( -
  • +
  • {renderLeaf(child, item.key)}
  • ))} @@ -285,12 +286,9 @@ const LeftSidebar = () => {
    diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyData/LeftSidebar/LeftSidebar.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MyData/LeftSidebar/LeftSidebar.test.tsx index 9df044fb1959..081d801e79c6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MyData/LeftSidebar/LeftSidebar.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyData/LeftSidebar/LeftSidebar.test.tsx @@ -11,6 +11,7 @@ * limitations under the License. */ import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { BrowserRouter } from 'react-router-dom'; import LeftSidebar from './LeftSidebar.component'; @@ -45,4 +46,30 @@ describe('LeftSidebar', () => { expect(screen.getByTestId('app-bar-item-settings')).toBeInTheDocument(); expect(screen.getByTestId('app-bar-item-logout')).toBeInTheDocument(); }); + + it.each([ + [ + 'click', + (user: ReturnType, el: HTMLElement) => + user.click(el), + ], + [ + 'Enter', + async (user: ReturnType, el: HTMLElement) => { + el.focus(); + await user.keyboard('{Enter}'); + }, + ], + ])('asks to confirm logout on %s', async (_, activate) => { + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + render( + + + + ); + + await activate(user, screen.getByTestId('app-bar-item-logout')); + + expect(await screen.findByTestId('confirm-logout')).toBeInTheDocument(); + }); }); From 34083c5574bfe92b0197811f7b7f91189f7a68cd Mon Sep 17 00:00:00 2001 From: Harshit Shah Date: Thu, 15 Oct 2026 16:24:12 +0530 Subject: [PATCH 22/34] fix(ui): let the classic sidebar sit on the page background Drop the white bg-surface the core migration added to the classic aside and keep solid-variant NavItemBase rest items transparent, so the rail shows the #f8f9fc page background like the pre-migration sidebar. --- .../app-navigation/base-components/nav-item.test.tsx | 7 +++++++ .../app-navigation/base-components/nav-item.tsx | 3 ++- .../MyData/LeftSidebar/LeftSidebar.component.tsx | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) 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 index cfab3741ad7c..391947598dfb 100644 --- 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 @@ -59,9 +59,16 @@ describe('NavItemBase', () => { Default + + Resting + ); + expect(screen.getByRole('link', { name: 'Resting' })).toHaveClass( + 'tw:bg-transparent' + ); + expect(screen.getByRole('link', { name: 'Current' })).toHaveClass( 'tw:bg-brand-solid' ); 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 aefccfaa2893..487c14c19e66 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 @@ -19,7 +19,8 @@ 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({ - root: 'tw:hover:bg-brand-primary_alt tw:dark:hover:bg-brand-primary_alt', + // 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: diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyData/LeftSidebar/LeftSidebar.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MyData/LeftSidebar/LeftSidebar.component.tsx index 6d6ae7a3f59e..b85e782581e0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MyData/LeftSidebar/LeftSidebar.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyData/LeftSidebar/LeftSidebar.component.tsx @@ -263,7 +263,7 @@ const LeftSidebar = () => { return (
    {renderExportProgress()} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/KPIFormFields.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/KPIFormFields.tsx index 7ac072afb769..68ec9fb7d761 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/KPIFormFields.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/KPIFormFields.tsx @@ -26,6 +26,7 @@ import { DateTime } from 'luxon'; import { UseFormReturn } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { KpiTargetType } from '../../generated/dataInsight/kpi/kpi'; +import { useStagedPickerValue } from '../../hooks/useStagedPickerValue'; import { dateValueToMillis, millisToPickerDateValue, @@ -79,6 +80,15 @@ const KPIFormFields = ({ form, chartItems, isEditing }: KPIFormFieldsProps) => { form.setValue('endDate', endDate, { shouldValidate: true }); }; + const startPicker = useStagedPickerValue( + toPickerValue(form.watch('startDate')), + (value) => handleStartDate(fromPickerValue(value)) + ); + const endPicker = useStagedPickerValue( + toPickerValue(form.watch('endDate')), + (value) => handleEndDate(fromPickerValue(value)) + ); + return ( <> { rules={{ required: t('label.field-required', { field: startLabel }), }}> - {({ field, fieldState }) => ( + {({ fieldState }) => ( { isInvalid={Boolean(fieldState.error)} minValue={todayValue} triggerVariant="input" - value={toPickerValue(field.value)} - onChange={(value) => handleStartDate(fromPickerValue(value))} + {...startPicker} /> {fieldState.error && ( {fieldState.error.message} @@ -238,7 +247,7 @@ const KPIFormFields = ({ form, chartItems, isEditing }: KPIFormFieldsProps) => { field: t('label.end-date'), }), }}> - {({ field, fieldState }) => ( + {({ fieldState }) => ( { isInvalid={Boolean(fieldState.error)} minValue={todayValue} triggerVariant="input" - value={toPickerValue(field.value)} - onChange={(value) => handleEndDate(fromPickerValue(value))} + {...endPicker} /> {fieldState.error && ( {fieldState.error.message} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.test.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.test.tsx index adef82e6da78..1a691ea00cff 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.test.tsx @@ -1048,6 +1048,61 @@ describe('CSV utils ClassBase', () => { ).not.toBeInTheDocument(); }); + it('should replace a single-value enum with the newly picked option', async () => { + mockGetTypeByFQN.mockResolvedValueOnce({ + customProperties: [ + { + name: 'status', + displayName: 'Status', + description: '', + propertyType: { name: 'enum' }, + customPropertyConfig: { + config: { multiSelect: false, values: ['Draft', 'Approved'] }, + }, + }, + ], + }); + const editor = csvUtils.getEditor( + 'extension', + EntityType.METRIC, + multipleOwner, + { usePlainTextEditor: true } + ); + const onRowChange = jest.fn(); + + if (!editor) { + throw new Error('Expected extension editor to be defined'); + } + + await act(async () => { + render( + <> + {editor({ + row: { extension: 'status:Draft' }, + column: { key: 'extension' }, + onRowChange, + onClose: jest.fn(), + } as unknown as Parameters[0])} + + ); + }); + + fireEvent.click( + within(await screen.findByTestId('status-enum-select')).getByRole( + 'button' + ) + ); + fireEvent.click(await screen.findByRole('option', { name: 'Approved' })); + fireEvent.click(screen.getByRole('button', { name: /update/i })); + + await waitFor(() => { + expect(onRowChange).toHaveBeenCalledWith( + { extension: 'status:Approved' }, + true + ); + }); + }); + it('should refetch metric custom properties after an empty response', async () => { const editor = csvUtils.getEditor( 'extension', diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.tsx index b00346af3705..d8602c86db2d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.tsx @@ -1479,13 +1479,32 @@ const InlineCustomPropertiesEditor = ({ const isMultiSelect = isCustomPropertyEnumMultiSelect(customProperty); const items = enumValues.map((option) => ({ id: option, label: option })); - const handleInserted = (key: Key) => - handleUpdateDraft( - customProperty.name, - isMultiSelect - ? [...new Set([...selectedValues, String(key)])] - : [String(key)] + // Single-value enums need to swap one value for another, which a chip + // Autocomplete refuses while it holds a value; a Select replaces natively. + if (!isMultiSelect) { + return ( + ); + } + + const handleInserted = (key: Key) => + handleUpdateDraft(customProperty.name, [ + ...new Set([...selectedValues, String(key)]), + ]); const handleCleared = (key: Key) => handleUpdateDraft( @@ -1495,11 +1514,11 @@ const InlineCustomPropertiesEditor = ({ return ( selectedValues.includes(item.id))} onItemCleared={handleCleared} From f54a592c4951604194ada26c89a15761ff982708 Mon Sep 17 00:00:00 2001 From: Harshit Shah Date: Fri, 16 Oct 2026 12:23:11 +0530 Subject: [PATCH 33/34] fix(ui): keep enum popovers inside the custom-property editor; let a single enum clear The editor cancels on any mousedown outside it or the bulk-edit-custom-property-select-dropdown class; the core Select and Autocomplete popovers portal to the body without that class, so a press in them could close the editor. A single-value enum also lost its own clear when it moved to Select; a clear button restores it. --- .../src/utils/CSV/CSVUtilsClassBase.test.tsx | 77 ++++++++++++++++++- .../ui/src/utils/CSV/CSVUtilsClassBase.tsx | 46 +++++++---- 2 files changed, 104 insertions(+), 19 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.test.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.test.tsx index 1a691ea00cff..0558c47f6e7c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.test.tsx @@ -18,6 +18,7 @@ import { waitFor, within, } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { FC } from 'react'; import { lazyTextEditor } from '../../components/common/DataGrid/LazyDataGrid'; import { DataAssetOption } from '../../components/DataAssets/DataAssetAsyncSelectList/DataAssetAsyncSelectList.interface'; @@ -1069,6 +1070,7 @@ describe('CSV utils ClassBase', () => { { usePlainTextEditor: true } ); const onRowChange = jest.fn(); + const onClose = jest.fn(); if (!editor) { throw new Error('Expected extension editor to be defined'); @@ -1081,19 +1083,26 @@ describe('CSV utils ClassBase', () => { row: { extension: 'status:Draft' }, column: { key: 'extension' }, onRowChange, - onClose: jest.fn(), + onClose, } as unknown as Parameters[0])} ); }); - fireEvent.click( + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + // userEvent sends the mousedown that the editor's outside-press guard + // listens for; fireEvent.click would skip it. + await user.click( within(await screen.findByTestId('status-enum-select')).getByRole( 'button' ) ); - fireEvent.click(await screen.findByRole('option', { name: 'Approved' })); - fireEvent.click(screen.getByRole('button', { name: /update/i })); + await user.click(await screen.findByRole('option', { name: 'Approved' })); + + // Picking inside the portalled list must not count as an outside press. + expect(onClose).not.toHaveBeenCalled(); + + await user.click(screen.getByRole('button', { name: /update/i })); await waitFor(() => { expect(onRowChange).toHaveBeenCalledWith( @@ -1103,6 +1112,66 @@ describe('CSV utils ClassBase', () => { }); }); + it('should clear a single-value enum on its own', async () => { + mockGetTypeByFQN.mockResolvedValueOnce({ + customProperties: [ + { + name: 'status', + displayName: 'Status', + description: '', + propertyType: { name: 'enum' }, + customPropertyConfig: { + config: { multiSelect: false, values: ['Draft', 'Approved'] }, + }, + }, + { + name: 'priority', + displayName: 'Priority', + description: '', + propertyType: { name: 'string' }, + }, + ], + }); + const editor = csvUtils.getEditor( + 'extension', + EntityType.METRIC, + multipleOwner, + { usePlainTextEditor: true } + ); + const onRowChange = jest.fn(); + + if (!editor) { + throw new Error('Expected extension editor to be defined'); + } + + await act(async () => { + render( + <> + {editor({ + row: { extension: 'status:Draft;priority:High' }, + column: { key: 'extension' }, + onRowChange, + onClose: jest.fn(), + } as unknown as Parameters[0])} + + ); + }); + + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + await user.click(await screen.findByTestId('status-enum-clear')); + + expect(screen.queryByTestId('status-enum-clear')).not.toBeInTheDocument(); + + await user.click(screen.getByRole('button', { name: /update/i })); + + await waitFor(() => { + expect(onRowChange).toHaveBeenCalledWith( + { extension: 'priority:High' }, + true + ); + }); + }); + it('should refetch metric custom properties after an empty response', async () => { const editor = csvUtils.getEditor( 'extension', diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.tsx index d8602c86db2d..2ff50da0a6a5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.tsx @@ -34,6 +34,7 @@ import { Sliders02, Tag01, Type01, + XClose, } from '@openmetadata/ui-core-components/icons'; import { isEmpty, startCase, toString } from 'lodash'; import { @@ -1482,22 +1483,36 @@ const InlineCustomPropertiesEditor = ({ // Single-value enums need to swap one value for another, which a chip // Autocomplete refuses while it holds a value; a Select replaces natively. if (!isMultiSelect) { + const selectedValue = selectedValues[0] ?? null; + return ( - + + + {selectedValue && ( + } + onClick={() => handleUpdateDraft(customProperty.name, [])} + /> + )} + ); } @@ -1520,6 +1535,7 @@ const InlineCustomPropertiesEditor = ({ data-testid={`${customProperty.name}-enum-select`} items={items} placeholder={t('label.enum-value-plural')} + popoverClassName="bulk-edit-custom-property-select-dropdown" selectedItems={items.filter((item) => selectedValues.includes(item.id))} onItemCleared={handleCleared} onItemInserted={handleInserted}> From cd72bd56bca21a4db4ecad69084f0c6de3359c88 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 16 Oct 2026 10:06:15 +0000 Subject: [PATCH 34/34] chore(playwright): auto-refresh impact-map.generated.json --- .github/playwright/impact-map.generated.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/playwright/impact-map.generated.json b/.github/playwright/impact-map.generated.json index 399f1ddfb688..7829f6ec070f 100644 --- a/.github/playwright/impact-map.generated.json +++ b/.github/playwright/impact-map.generated.json @@ -803,6 +803,7 @@ "playwright/e2e/Features/DataQuality/Profiler.spec.ts", "playwright/e2e/Features/DataQuality/ProfilerIngestionForm.spec.ts", "playwright/e2e/Features/DataQuality/RunTestCase.spec.ts", + "playwright/e2e/Features/DataQuality/TableTestCaseExportFilters.spec.ts", "playwright/e2e/Features/DataQuality/TableTestCasePagination.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseDetailsPage.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseImportExportBasic.spec.ts", @@ -1711,6 +1712,7 @@ "playwright/e2e/Features/DataQuality/ProfilerSettingsAfterReindex.spec.ts", "playwright/e2e/Features/DataQuality/RunTestCase.spec.ts", "playwright/e2e/Features/DataQuality/TableLevelTests.spec.ts", + "playwright/e2e/Features/DataQuality/TableTestCaseExportFilters.spec.ts", "playwright/e2e/Features/DataQuality/TableTestCasePagination.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseCrudAfterReindex.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseDetailsPage.spec.ts", @@ -2024,6 +2026,7 @@ "playwright/e2e/Features/DataAssetRulesEnabled.spec.ts", "playwright/e2e/Features/DataQuality/ProfilerIngestionForm.spec.ts", "playwright/e2e/Features/DataQuality/TableLevelTests.spec.ts", + "playwright/e2e/Features/DataQuality/TableTestCaseExportFilters.spec.ts", "playwright/e2e/Features/ExploreQuickFilters.spec.ts", "playwright/e2e/Features/IngestionListNameSorting.spec.ts", "playwright/e2e/Features/LandingPageWidgets/LandingPagePlatformHealth.spec.ts", @@ -3074,6 +3077,7 @@ "playwright/e2e/Features/DataQuality/Profiler.spec.ts", "playwright/e2e/Features/DataQuality/ProfilerIngestionForm.spec.ts", "playwright/e2e/Features/DataQuality/RunTestCase.spec.ts", + "playwright/e2e/Features/DataQuality/TableTestCaseExportFilters.spec.ts", "playwright/e2e/Features/DataQuality/TableTestCasePagination.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseDetailsPage.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseImportExportBasic.spec.ts", @@ -3296,6 +3300,7 @@ "playwright/e2e/Features/AdvancedSearch.spec.ts", "playwright/e2e/Features/ContextCenterDashboard.spec.ts", "playwright/e2e/Features/DataQuality/IncidentManagerSqlQuery.spec.ts", + "playwright/e2e/Features/DataQuality/TableTestCaseExportFilters.spec.ts", "playwright/e2e/Features/DomainFilterQueryFilter.spec.ts", "playwright/e2e/Features/LandingPageWidgets/LandingPageContextCenter.spec.ts", "playwright/e2e/Features/LandingPageWidgets/LandingPageCriticalAnnouncements.spec.ts", @@ -3486,6 +3491,7 @@ "playwright/e2e/Features/DataQuality/ProfilerSettingsAfterReindex.spec.ts", "playwright/e2e/Features/DataQuality/RunTestCase.spec.ts", "playwright/e2e/Features/DataQuality/TableLevelTests.spec.ts", + "playwright/e2e/Features/DataQuality/TableTestCaseExportFilters.spec.ts", "playwright/e2e/Features/DataQuality/TableTestCasePagination.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseCrudAfterReindex.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseDetailsPage.spec.ts", @@ -4101,6 +4107,7 @@ "playwright/e2e/Features/DataQuality/DataQualityListAfterReindex.spec.ts", "playwright/e2e/Features/DataQuality/DataQualityPermissions.spec.ts", "playwright/e2e/Features/DataQuality/IncidentManagerDateFilter.spec.ts", + "playwright/e2e/Features/DataQuality/TableTestCaseExportFilters.spec.ts", "playwright/e2e/Features/DataQuality/TableTestCasePagination.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseDetailsPage.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseImportExportBasic.spec.ts", @@ -4834,6 +4841,7 @@ "playwright/e2e/Features/AIMode/ConnectionServiceDetails.spec.ts", "playwright/e2e/Features/Announcements/SystemAnnouncements.spec.ts", "playwright/e2e/Features/BulkEditEntity.spec.ts", + "playwright/e2e/Features/DataQuality/TableTestCaseExportFilters.spec.ts", "playwright/e2e/Features/OnlineUsers.spec.ts", "playwright/e2e/Features/TestSuitePipelineRedeploy.spec.ts", "playwright/e2e/Pages/AIProfile/ProfileMembers.spec.ts", @@ -5161,6 +5169,7 @@ "playwright/e2e/Features/DataQuality/Profiler.spec.ts", "playwright/e2e/Features/DataQuality/RunTestCase.spec.ts", "playwright/e2e/Features/DataQuality/TableLevelTests.spec.ts", + "playwright/e2e/Features/DataQuality/TableTestCaseExportFilters.spec.ts", "playwright/e2e/Features/DataQuality/TableTestCasePagination.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseCrudAfterReindex.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseDetailsPage.spec.ts", @@ -5269,6 +5278,7 @@ "playwright/e2e/Features/DataQuality/DataQuality.spec.ts", "playwright/e2e/Features/DataQuality/IncidentManagerDateFilter.spec.ts", "playwright/e2e/Features/DataQuality/IncidentManagerSqlQuery.spec.ts", + "playwright/e2e/Features/DataQuality/TableTestCaseExportFilters.spec.ts", "playwright/e2e/Features/DataQuality/TableTestCasePagination.spec.ts", "playwright/e2e/Features/EntitySuggestions.spec.ts", "playwright/e2e/Features/FailedTestCaseSampleData.spec.ts", @@ -6782,6 +6792,14 @@ "playwright/e2e/Features/DataQuality/ProfilerSettingsAfterReindex.spec.ts" ] }, + { + "sources": [ + "openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/QualityTab/QualityTab.component.tsx" + ], + "specs": [ + "playwright/e2e/Features/DataQuality/TableTestCaseExportFilters.spec.ts" + ] + }, { "sources": [ "openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummary/RunSummaryTiles/RunSummaryTiles.tsx" @@ -11962,6 +11980,7 @@ "playwright/e2e/Features/DataQuality/ProfilerSettingsAfterReindex.spec.ts", "playwright/e2e/Features/DataQuality/RunTestCase.spec.ts", "playwright/e2e/Features/DataQuality/TableLevelTests.spec.ts", + "playwright/e2e/Features/DataQuality/TableTestCaseExportFilters.spec.ts", "playwright/e2e/Features/DataQuality/TableTestCasePagination.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseCrudAfterReindex.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseDetailsPage.spec.ts",