+ )}
+
+
);
};
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 46907e3ee7d6..a9aea16e465a 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
@@ -19,12 +19,10 @@ import {
Dropdown,
Grid,
} from '@openmetadata/ui-core-components';
-import { XCircle } 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 +41,7 @@ import {
getEpochMillisForPastDays,
} from '../../../utils/date-time/DateTimeUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
-import DatePicker, {
- RangePickerProps,
-} from '../../common/DatePicker/DatePicker';
-import './execution.less';
+import DateRangeFilter from '../../common/DateRangeFilter/DateRangeFilter';
import ListView from './ListView/ListViewTab.component';
import TreeViewTab from './TreeView/TreeViewTab.component';
@@ -92,33 +87,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 +169,15 @@ const ExecutionsTab = ({ pipelineFQN, tasks }: ExecutionProps) => {
{view === PIPELINE_EXECUTION_TABS.LIST_VIEW ? (
-
}
- data-testid="data-range-picker"
- placeholder={[t('label.date-filter'), '']}
- suffixIcon={
}
- onChange={onDateChange}
- />
+
+ 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/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 }));
}),
}));
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 (
- : null}
- options={optionsInternal}
- searchValue={searchText}
- suffixIcon={loadingOptions && }
- onPopupScroll={enableInfiniteScroll ? onScroll : undefined}
- onSearch={(value: string) => {
- setSearchText(value);
- setCurrentPage(1);
- }}
- onSelect={handleSelection}
- {...restProps}
- />
- );
-};
-
-// Re-exported because consumers outside this repository import it from this path.
-export type { PagingResponse } from '../../../interface/common/paging.interface';
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelect/AsyncSelectList.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelect/AsyncSelectList.interface.ts
deleted file mode 100644
index ff5558c7bef2..000000000000
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelect/AsyncSelectList.interface.ts
+++ /dev/null
@@ -1,23 +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 { DefaultOptionType } from 'antd/lib/select';
-import { PagingResponse } from '../../../interface/common/paging.interface';
-
-export interface AsyncSelectListProps {
- api: (
- queryString: string,
- page?: number
- ) => Promise>;
- enableInfiniteScroll?: boolean;
- debounceTimeout?: number;
-}
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/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 63%
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..8861968933b7 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,14 +39,23 @@ 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;
}
+const toPickerRange = (startTs?: number, endTs?: number): PickerRange => {
+ const start = millisToPickerDateValue(startTs);
+ const end = millisToPickerDateValue(endTs);
+
+ return start && end ? { start, end } : null;
+};
+
/**
* Adapts the core (untitled-ui) `DateRangePicker`, which speaks
* `@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,21 +63,16 @@ export const DqDateRangeFilter = ({
isOpen,
onOpenChange,
onApply,
-}: DqDateRangeFilterProps) => {
- const buildValue = (): PickerRange => {
- const start = millisToDateValue(startTs);
- const end = millisToDateValue(endTs);
-
- return start && end ? { start, end } : null;
- };
-
- const [value, setValue] = useState(buildValue);
+ onClear,
+}: DateRangeFilterProps) => {
+ const { t } = useTranslation();
+ const [value, setValue] = useState(() =>
+ toPickerRange(startTs, endTs)
+ );
// Re-sync the staged value when the committed range changes upstream.
useEffect(() => {
- const start = millisToDateValue(startTs);
- const end = millisToDateValue(endTs);
- setValue(start && end ? { start, end } : null);
+ setValue(toPickerRange(startTs, endTs));
}, [startTs, endTs]);
// Discard any uncommitted (staged) selection when single-open coordination
@@ -71,9 +82,9 @@ export const DqDateRangeFilter = ({
// Apply / Cancel) are handled by `handleOpenChange` instead (see below).
useEffect(() => {
if (isOpen === false) {
- setValue(buildValue());
+ setValue(toPickerRange(startTs, endTs));
}
- }, [isOpen]);
+ }, [isOpen, startTs, endTs]);
// Intercept every close event — including uncontrolled usage where the
// caller passes no `isOpen`/`onOpenChange` — so an abandoned (never-applied)
@@ -82,27 +93,25 @@ export const DqDateRangeFilter = ({
// report here; together they cover both close origins.
const handleOpenChange = (open: boolean) => {
if (!open) {
- setValue(buildValue());
+ setValue(toPickerRange(startTs, endTs));
}
onOpenChange?.(open);
};
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),
});
}
};
const handleCancel = () => {
- setValue(buildValue());
+ setValue(toPickerRange(startTs, endTs));
};
- 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/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/components/common/EntityPageInfos/ManageButton/ManageButton.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/ManageButton/ManageButton.tsx
index 7e27d017f32b..4a541a34c722 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/ManageButton/ManageButton.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/ManageButton/ManageButton.tsx
@@ -34,10 +34,7 @@ import { hardDeleteEntity } from '../../../../utils/DeleteWidget/DeleteWidgetUti
import entityUtilClassBase from '../../../../utils/EntityUtilClassBase';
import { showErrorToast } from '../../../../utils/ToastUtils';
import EntityNameModal from '../../../Modals/EntityNameModal/EntityNameModal.component';
-import {
- EntityName,
- EntityNameValidationRule,
-} from '../../../Modals/EntityNameModal/EntityNameModal.interface';
+import { EntityName } from '../../../Modals/EntityNameModal/EntityNameModal.interface';
import DeleteModal from '../../DeleteModal/DeleteModal';
import DeleteEntityModal from '../../DeleteWidget/DeleteEntityModal';
import { DeleteType } from '../../DeleteWidget/DeleteWidget.interface';
@@ -382,9 +379,7 @@ const ManageButton: FC = ({
{onEditDisplayName && isDisplayNameEditing && (
{
- it('FilterPattern component should render', async () => {
- const { container } = render();
-
- const filterPatternContainer = await findByTestId(
- container,
- 'filter-pattern-container'
- );
- const fieldContainer = await findByTestId(container, 'field-container');
- const checkbox = await findByTestId(
- container,
- `${mockFilterPatternProps.type}-filter-pattern-checkbox`
- );
- const includeFilterInput = await findByTestId(
- container,
- 'filter-pattern-includes-table'
- );
- const excludeFilterInput = await findByTestId(
- container,
- 'filter-pattern-excludes-table'
- );
-
- expect(filterPatternContainer).toBeInTheDocument();
- expect(checkbox).toBeInTheDocument();
- expect(fieldContainer).toBeInTheDocument();
- expect(includeFilterInput).toBeInTheDocument();
- expect(excludeFilterInput).toBeInTheDocument();
- });
-
- it('FilterPattern component should render with filter pattern description', async () => {
- const { container } = render();
-
- const filterPatternContainer = await findByTestId(
- container,
- 'filter-pattern-container'
- );
- const fieldContainer = await findByTestId(container, 'field-container');
- const checkbox = await findByTestId(
- container,
- `${mockFilterPatternProps.type}-filter-pattern-checkbox`
- );
-
- const includeFilterInput = await findByTestId(
- container,
- 'filter-pattern-includes-table'
- );
- const excludeFilterInput = await findByTestId(
- container,
- 'filter-pattern-excludes-table'
- );
-
- expect(filterPatternContainer).toBeInTheDocument();
- expect(checkbox).toBeInTheDocument();
- expect(fieldContainer).toBeInTheDocument();
- expect(includeFilterInput).toBeInTheDocument();
- expect(excludeFilterInput).toBeInTheDocument();
- });
-});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/FilterPattern/FilterPattern.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/FilterPattern/FilterPattern.tsx
deleted file mode 100644
index 2e7c2963bcfb..000000000000
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/FilterPattern/FilterPattern.tsx
+++ /dev/null
@@ -1,126 +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 {
- Box,
- Checkbox,
- Divider,
- Grid,
- Typography,
-} from '@openmetadata/ui-core-components';
-import { Select } from 'antd';
-import { capitalize } from 'lodash';
-import { useTranslation } from 'react-i18next';
-import { getLayoutGutter } from '../../../utils/common/layout.utils';
-import { FilterPatternProps } from './filterPattern.interface';
-
-const FilterPattern = ({
- showSeparator = true,
- isDisabled = false,
- checked,
- includePattern,
- excludePattern,
- handleChecked,
- getIncludeValue,
- getExcludeValue,
- includePatternExtraInfo,
- type,
-}: FilterPatternProps) => {
- const { t } = useTranslation();
-
- return (
-
-
-
- {/* eslint-disable-next-line jsx-a11y/label-has-for -- htmlFor-linked to checkbox (sibling column) */}
-
-
-
- handleChecked(e)}
- />
-
-
- {checked && (
-
-
-
- {t('label.include')}:
-
-
-
-
-
- {t('label.exclude')}:
-
-
-
- )}
-
- );
-};
-
-export default FilterPattern;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/FilterPattern/filterPattern.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/FilterPattern/filterPattern.interface.ts
deleted file mode 100644
index 6650dbcdfa6b..000000000000
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/FilterPattern/filterPattern.interface.ts
+++ /dev/null
@@ -1,27 +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 { FilterPatternEnum } from '../../../enums/filterPattern.enum';
-
-export interface FilterPatternProps {
- checked: boolean;
- isDisabled?: boolean;
- includePatternExtraInfo?: string;
- showSeparator?: boolean;
- handleChecked: (e: boolean) => void;
- includePattern: Array | undefined;
- excludePattern: Array | undefined;
- type: FilterPatternEnum;
- getExcludeValue: (value: Array, type: FilterPatternEnum) => void;
- getIncludeValue: (value: Array, type: FilterPatternEnum) => void;
-}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/SanitizedInput/SanitizedInput.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/SanitizedInput/SanitizedInput.test.tsx
deleted file mode 100644
index 855273e9b9b8..000000000000
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/SanitizedInput/SanitizedInput.test.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright 2024 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 } from '@testing-library/react';
-import { getSanitizeContent } from '../../../utils/sanitize.utils';
-import SanitizedInput from './SanitizedInput';
-
-jest.mock('../../../utils/sanitize.utils');
-
-describe('SanitizedInput', () => {
- it('should render the input component', () => {
- const { getByPlaceholderText } = render(
-
- );
-
- expect(getByPlaceholderText('Enter text')).toBeInTheDocument();
- });
-
- it('should call getSanitizeContent and onChange with sanitized value', () => {
- const mockSanitizeContent = getSanitizeContent as jest.Mock;
- mockSanitizeContent.mockReturnValue('sanitized value');
-
- const handleChange = jest.fn();
- const { getByPlaceholderText } = render(
-
- );
-
- const input = getByPlaceholderText('Enter text');
- fireEvent.change(input, { target: { value: 'unsanitized value' } });
-
- expect(mockSanitizeContent).toHaveBeenCalledWith('unsanitized value');
- expect(handleChange).toHaveBeenCalledWith(
- expect.objectContaining({
- target: expect.objectContaining({ value: 'sanitized value' }),
- })
- );
- });
-});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/SanitizedInput/SanitizedInput.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/SanitizedInput/SanitizedInput.tsx
deleted file mode 100644
index 463858e9ab9a..000000000000
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/SanitizedInput/SanitizedInput.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright 2024 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 { Input, InputProps } from 'antd';
-import { ChangeEvent, FC, memo, useCallback } from 'react';
-import { getSanitizeContent } from '../../../utils/sanitize.utils';
-
-const SanitizedInput: FC = ({ value, onChange, ...props }) => {
- const handleChange = useCallback(
- (e: ChangeEvent) => {
- const sanitizedValue = getSanitizeContent(e.target.value);
- if (onChange) {
- onChange({ ...e, target: { ...e.target, value: sanitizedValue } });
- }
- },
- [onChange]
- );
-
- return ;
-};
-
-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 (
-
- );
- }, [
- disabled,
- selectedValues,
- customTagRender,
- handleOnChangeSelect,
- handleFocus,
- ]);
-
- useEffect(() => {
- setSelectedUsers(owner ?? []);
- }, [owner]);
-
- return (
- <>
- {popoverVisible && (
-
- {/* Have to pass the selectInput as children, so popover can become targetComponent
- and popover don't overflow on it */}
- {selectInput}
-
- )}
- {/* Conditionally render the select input, to avoid the UserTeamSelectableList component
- render unnecessarily */}
- {!popoverVisible && selectInput}
- >
- );
-};
-
-export default UserTeamSelectableListSearchInput;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/UserTeamSelectableListSearchInput/user-team-selectable-list-search-input.less b/openmetadata-ui/src/main/resources/ui/src/components/common/UserTeamSelectableListSearchInput/user-team-selectable-list-search-input.less
deleted file mode 100644
index 7cb801ca3a3a..000000000000
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/UserTeamSelectableListSearchInput/user-team-selectable-list-search-input.less
+++ /dev/null
@@ -1,52 +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 (reference) '../../../styles/variables.less';
-
-.ant-popover.user-team-selectable-list-search-input-popover {
- width: calc(50vw - 50px);
- min-width: 260px;
- max-width: 600px;
-}
-
-.select-owners {
- .assignee-tag {
- margin-right: var(--om-space-8);
- background: var(--tw-color-bg-primary);
- padding: var(--om-space-2) var(--om-space-8);
- height: 24px;
- border-radius: var(--om-radius-md);
- border: 1px solid @grey-300;
-
- .ant-typography {
- font-size: var(--om-font-size-sm);
- font-weight: var(--om-font-weight-medium);
- color: @grey-700;
- }
-
- .anticon {
- color: @grey-400;
- font-weight: var(--om-font-weight-medium);
- font-size: var(--om-font-size-8);
- }
-
- .ant-select-selection-item-content {
- display: flex;
- align-items: center;
- }
-
- .ant-select-selection-item-remove {
- margin-top: var(--om-space-2);
- }
- }
-}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/observability/Alerts/AlertAiFormFieldsPureUtils.ts b/openmetadata-ui/src/main/resources/ui/src/components/observability/Alerts/AlertAiFormFieldsPureUtils.ts
index da0b94c9026f..ab5af45eb7d8 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/observability/Alerts/AlertAiFormFieldsPureUtils.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/components/observability/Alerts/AlertAiFormFieldsPureUtils.ts
@@ -501,8 +501,8 @@ export const getDestinationWithNotifyDownstream = (
/**
* Event types offered by the event-type filter. Only the Settings → Notifications
- * flow narrows them to the source's supported types (classic
- * `AddNotificationPage`); the Observability flow always offered every type.
+ * flow narrows them to the source's supported types; the Observability flow
+ * always offered every type.
*/
export const getRuleEventTypes = (
alertType: AlertType | undefined,
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/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) => (
= [
},
];
-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/constants/Form.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/Form.constants.ts
index f18a83da4328..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
@@ -11,8 +11,7 @@
* limitations under the License.
*/
-import { Rule } from 'antd/lib/form';
-
+import type { EntityNameValidationRule } from '../components/Modals/EntityNameModal/EntityNameModal.interface';
import i18n from '../utils/i18next/LocalUtil';
import { ENTITY_NAME_REGEX } from './regex.constants';
@@ -34,19 +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');
-// TODO(open-metadata/openmetadata-collate#6893): drop the antd `Rule[]` shapes
-// once the last antd forms (alerts, custom metrics) move to react-hook-form.
-export const NAME_FIELD_RULES: Rule[] = [
- { 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: Rule[] = [
+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 f0ddf6fa8f89..32fc216fb602 100644
--- a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts
@@ -474,38 +474,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/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/hooks/useStagedPickerValue.test.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useStagedPickerValue.test.ts
new file mode 100644
index 000000000000..4b0f83840185
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useStagedPickerValue.test.ts
@@ -0,0 +1,74 @@
+/*
+ * 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 { act, renderHook } from '@testing-library/react';
+import { useStagedPickerValue } from './useStagedPickerValue';
+
+const setup = (committed: string | null = 'saved') => {
+ const commit = jest.fn();
+ const hook = renderHook(() => useStagedPickerValue(committed, commit));
+
+ return { commit, hook };
+};
+
+describe('useStagedPickerValue', () => {
+ it('shows a pick without committing it until Apply', () => {
+ const { commit, hook } = setup();
+
+ act(() => hook.result.current.onChange('picked'));
+
+ expect(hook.result.current.value).toBe('picked');
+ expect(commit).not.toHaveBeenCalled();
+
+ act(() => hook.result.current.onApply());
+
+ expect(commit).toHaveBeenCalledWith('picked');
+ });
+
+ it.each([
+ [
+ 'Cancel',
+ (h: ReturnType>) =>
+ h.onCancel(),
+ ],
+ [
+ 'a dismissed popover',
+ (h: ReturnType>) =>
+ h.onOpenChange(false),
+ ],
+ ])('restores the saved value on %s', (_, close) => {
+ const { commit, hook } = setup();
+
+ act(() => hook.result.current.onChange('picked'));
+ act(() => close(hook.result.current));
+
+ expect(hook.result.current.value).toBe('saved');
+ expect(commit).not.toHaveBeenCalled();
+ });
+
+ it('commits a cleared pick', () => {
+ const { commit, hook } = setup();
+
+ act(() => hook.result.current.onChange(null));
+ act(() => hook.result.current.onApply());
+
+ expect(commit).toHaveBeenCalledWith(null);
+ });
+
+ it('does not commit when Apply is pressed with nothing picked', () => {
+ const { commit, hook } = setup();
+
+ act(() => hook.result.current.onApply());
+
+ expect(commit).not.toHaveBeenCalled();
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useStagedPickerValue.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useStagedPickerValue.ts
new file mode 100644
index 000000000000..002da9649790
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useStagedPickerValue.ts
@@ -0,0 +1,43 @@
+/*
+ * 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 { useState } from 'react';
+
+/**
+ * Core date pickers show Apply and Cancel but report every pick through
+ * `onChange`. Spread the result on a picker to hold picks locally, commit them
+ * on Apply, and drop them on Cancel or any other close.
+ */
+export const useStagedPickerValue = (
+ committed: T,
+ commit: (value: T) => void
+) => {
+ // `undefined` means nothing is staged; `null` is a real (cleared) pick.
+ const [draft, setDraft] = useState(undefined);
+
+ return {
+ value: draft === undefined ? committed : draft,
+ onChange: (value: T) => setDraft(value),
+ onApply: () => {
+ if (draft !== undefined) {
+ commit(draft);
+ }
+ setDraft(undefined);
+ },
+ onCancel: () => setDraft(undefined),
+ onOpenChange: (isOpen: boolean) => {
+ if (!isOpen) {
+ setDraft(undefined);
+ }
+ },
+ };
+};
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..d0678804f96a
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useThemeCssVariables.test.ts
@@ -0,0 +1,73 @@
+/*
+ * 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('');
+ });
+
+ 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/context/AntDConfigProvider/AntDConfigProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/hooks/useThemeCssVariables.ts
similarity index 53%
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..14f73bb7f609 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,51 @@
* 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 { generatePalette } from '../styles/colorPallet';
+import { useApplicationStore } from './useApplicationStore';
-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);
+ } else {
+ // A cleared colour falls back to the stylesheet default again.
+ document.documentElement.style.removeProperty(variable);
+ }
+ });
+ }, [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 +96,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/interface/FormUtils.interface.ts b/openmetadata-ui/src/main/resources/ui/src/interface/FormUtils.interface.ts
index af3908a6d6c3..1a1b1a2a8bef 100644
--- a/openmetadata-ui/src/main/resources/ui/src/interface/FormUtils.interface.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/interface/FormUtils.interface.ts
@@ -11,9 +11,6 @@
* limitations under the License.
*/
-import { FormItemProps, FormRule } from 'antd';
-import { NamePath } from 'antd/lib/form/interface';
-import { ReactNode } from 'react';
import { FormValidationRulesType } from '../enums/form.enum';
export type FormValidationRules = Record<
@@ -21,55 +18,7 @@ export type FormValidationRules = Record<
Array
>;
-export enum FormItemLayout {
- HORIZONTAL = 'horizontal',
- VERTICAL = 'vertical',
-}
-
-export enum FieldTypes {
- TEXT = 'text',
- UT_TEXT = 'ut_text',
- PASSWORD = 'password',
- FILTER_PATTERN = 'filter_pattern',
- SWITCH = 'switch',
- UT_SWITCH = 'ut_switch',
- SELECT = 'select',
- UT_SELECT = 'ut_select',
- ASYNC_SELECT_LIST = 'async_select_list',
- NUMBER = 'number',
- CHECK_BOX = 'check_box',
- SLIDER_INPUT = 'slider_input',
- DESCRIPTION = 'description',
- TAG_SUGGESTION = 'tag_suggestion',
- USER_TEAM_SELECT = 'user_team_select',
- USER_TEAM_SELECT_INPUT = 'user_team_select_input',
- USER_MULTI_SELECT = 'user_multi_select',
- COLOR_PICKER = 'color_picker',
- DOMAIN_SELECT = 'domain_select',
- CRON_EDITOR = 'cron_editor',
- COMPONENT = 'component',
-}
-
export enum HelperTextType {
ALERT = 'alert',
Tooltip = 'tooltip',
}
-
-export interface FieldProp {
- label: ReactNode;
- name: NamePath;
- type: FieldTypes;
- required: boolean;
- id: string;
- props?: Record & { children?: ReactNode };
- formItemProps?: FormItemProps;
- rules?: FormRule[];
- helperText?: ReactNode;
- helperTextType?: HelperTextType;
- showHelperText?: boolean;
- placeholder?: string;
- hasSeparator?: boolean;
- formItemLayout?: FormItemLayout;
- isBeta?: boolean;
- newLook?: boolean;
-}
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
deleted file mode 100644
index c1ce13db355d..000000000000
--- a/openmetadata-ui/src/main/resources/ui/src/pages/AddNotificationPage/AddNotificationPage.test.tsx
+++ /dev/null
@@ -1,307 +0,0 @@
-/*
- * Copyright 2024 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 { act, fireEvent, render, screen } from '@testing-library/react';
-import { MemoryRouter } from 'react-router-dom';
-import { getAllNotificationTemplates } from '../../rest/notificationtemplateAPI';
-import AddNotificationPage from './AddNotificationPage';
-
-const mockNavigate = jest.fn();
-const mockGetResourcePermission = jest.fn();
-const mockHandleAlertSave = jest.fn();
-const mockGetModifiedAlertDataForForm = jest.fn();
-
-jest.mock('../../utils/AlertsClassBase', () => ({
- __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/DestinationFormItemFormBridge',
- () =>
- 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 6602fa755f5c..000000000000
--- a/openmetadata-ui/src/main/resources/ui/src/pages/AddNotificationPage/AddNotificationPage.tsx
+++ /dev/null
@@ -1,646 +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 {
- 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 { 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 { 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[] = [];
-
-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 { supportedFilters } = selection.support;
-
- 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) {
- return ;
- }
-
- if (isSystemProvider) {
- return (
-
-
- {t('message.system-alert-edit-message')}
-
-
- );
- }
-
- return (
-
-
-
-
-
-
-
-
-
- {t(`label.${isEditMode ? 'edit' : 'add'}-entity`, {
- entity: t('label.alert'),
- })}
-
- {t('message.alerts-description')}
-
-
-
-
-
-
-
-
-
- ),
- 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..8544e0e8148d 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: <>>,
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/AppInstall/AppInstall.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.test.tsx
index 4166e309d0d2..af5fed148a6e 100644
--- a/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.test.tsx
@@ -146,10 +146,6 @@ jest.mock('../../rest/applicationMarketPlaceAPI', () => ({
jest.mock('../../utils/EntityDisplayPureUtils', () => ({
getEntityMissingError: jest.fn(),
}));
-jest.mock('../../utils/SchedularUtils', () => ({
- getIngestionFrequency: jest.fn(),
-}));
-
jest.mock('../../utils/JSONSchemaFormUtils', () => ({
formatFormDataForSubmit: jest.fn(() => mockFormatFormDataForSubmit()),
}));
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..78e853ec5cb5 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,31 @@ jest.mock('@openmetadata/ui-core-components', () => ({
.mockImplementation(({ children, 'data-testid': testId }) => (
{children}
)),
+ DateRangePicker: jest
+ .fn()
+ .mockImplementation(({ onChange, onApply, 'data-testid': testId }) => {
+ const day = {
+ toDate: () => new Date(0),
+ add: () => ({ toDate: () => new Date(86_400_000) }),
+ };
+
+ return (
+ <>
+ onChange({ start: day, end: day })}>
+ range
+
+ onApply?.()}>
+ apply
+
+ >
+ );
+ }),
Input: jest
.fn()
.mockImplementation(({ value, onChange, inputDataTestId, placeholder }) => (
@@ -147,29 +172,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;
- }) => (
- onChange([mockRangeDay, mockRangeDay])}>
- range
-
- ),
- },
-}));
-
jest.mock('../../components/PageHeader/PageHeader.component', () =>
jest.fn().mockReturnValue(Page Header
)
);
@@ -822,6 +824,9 @@ describe('AuditLogsPage', () => {
await act(async () => {
fireEvent.click(screen.getByTestId('export-date-range-picker'));
});
+ await act(async () => {
+ fireEvent.click(screen.getByTestId('export-date-range-picker-apply'));
+ });
await act(async () => {
fireEvent.click(
within(screen.getByRole('dialog')).getByRole('button', {
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..f126dea84999 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';
@@ -51,6 +50,7 @@ import { PAGE_HEADERS } from '../../constants/PageHeaders.constant';
import { useWebSocketConnector } from '../../context/WebSocketProvider/WebSocketProvider';
import { CursorType } from '../../enums/pagination.enum';
import { Paging } from '../../generated/type/paging';
+import { useStagedPickerValue } from '../../hooks/useStagedPickerValue';
import {
exportAuditLogs,
getAuditLogExportJob,
@@ -64,11 +64,18 @@ import {
AuditLogListResponse,
} from '../../types/auditLogs.interface';
import { buildParamsFromFilters } from '../../utils/AuditLogUtils';
+import {
+ dateValueToEndOfDayMillis,
+ dateValueToMillis,
+ millisToPickerDateValue,
+} 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';
+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,12 +114,15 @@ 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);
+ const exportRangePicker = useStagedPickerValue(
+ exportDateRange,
+ setExportDateRange
+ );
const fetchAuditLogs = useCallback(
async (
@@ -386,8 +396,8 @@ 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),
+ endTs: dateValueToEndOfDayMillis(exportDateRange.end),
q: searchTerm || undefined,
...filterParams,
});
@@ -624,21 +634,12 @@ const AuditLogsPage = () => {
size="text-md">
{t('label.date-range')} *
- current > DateTime.now().endOf('day')}
- getPopupContainer={getPopupContainer}
- value={exportDateRange}
- onChange={(dates) => {
- if (dates?.[0] && dates?.[1]) {
- setExportDateRange([dates[0], dates[1]]);
- } else {
- setExportDateRange(null);
- }
- }}
+ isDisabled={isExporting}
+ maxValue={millisToPickerDateValue(Date.now())}
+ {...exportRangePicker}
/>
{renderExportProgress()}
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/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..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
@@ -13,6 +13,7 @@
import {
Box,
+ DatePicker,
FormField,
FormItemLabel,
Grid,
@@ -24,13 +25,22 @@ 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 { useStagedPickerValue } from '../../hooks/useStagedPickerValue';
+import {
+ dateValueToMillis,
+ millisToPickerDateValue,
+ PickerDateValue,
+} from '../../utils/date-time/calendarDate.utils';
import { KPIMetricTypeOptions } from '../../utils/KPI/KPIUtils';
import { KPIFormValues } from './KPIPage.interface';
+const toPickerValue = (value: DateTime | null) =>
+ millisToPickerDateValue(value?.toMillis());
+
+const fromPickerValue = (value: PickerDateValue | null | undefined) =>
+ value ? DateTime.fromMillis(dateValueToMillis(value)) : null;
+
interface KPIFormFieldsProps {
form: UseFormReturn;
chartItems: Array<{ id: string; label: string }>;
@@ -48,6 +58,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) => {
@@ -69,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 (
<>
{
items={chartItems}
label={t('label.select-a-chart')}
placeholder={t('label.select-a-chart')}
- selectedKey={field.value}
- onSelectionChange={(key) =>
- key !== null && field.onChange(String(key))
- }>
+ value={field.value}
+ onChange={(key) => key !== null && field.onChange(String(key))}>
{(item) => }
)}
@@ -118,8 +136,8 @@ const KPIFormFields = ({ form, chartItems, isEditing }: KPIFormFieldsProps) => {
isDisabled={isEditing}
items={METRIC_TYPE_ITEMS}
label={t('label.metric-type')}
- selectedKey={field.value}
- onSelectionChange={(key) =>
+ value={field.value}
+ onChange={(key) =>
key !== null && field.onChange(key as KpiTargetType)
}>
{(item) => }
@@ -160,7 +178,7 @@ const KPIFormFields = ({ form, chartItems, isEditing }: KPIFormFieldsProps) => {
type="number"
value={String(field.value ?? '')}
onChange={(value) =>
- field.onChange(value === '' ? NaN : Number(value))
+ field.onChange(value === '' ? Number.NaN : Number(value))
}
/>
);
@@ -201,17 +219,17 @@ const KPIFormFields = ({ form, chartItems, isEditing }: KPIFormFieldsProps) => {
rules={{
required: t('label.field-required', { field: startLabel }),
}}>
- {({ field, fieldState }) => (
+ {({ fieldState }) => (
{fieldState.error && (
{fieldState.error.message}
@@ -229,17 +247,17 @@ const KPIFormFields = ({ form, chartItems, isEditing }: KPIFormFieldsProps) => {
field: t('label.end-date'),
}),
}}>
- {({ field, fieldState }) => (
+ {({ fieldState }) => (
{fieldState.error && (
{fieldState.error.message}
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(),
}));
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..a38b18ff54d3 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,136 +154,134 @@ const AddTeamForm: React.FC = ({
width={750}
onCancel={onCancel}
onOk={form.handleSubmit(handleSubmit)}>
-
-
- {renderTextField(
- 'name',
- t('label.name'),
- t('label.enter-entity', { entity: t('label.name') }),
- 'name',
- {
- ...NAME_FIELD_REGISTER_RULES,
- validate: (value) =>
- !allTeam.some(
- (team) => toLower(team.name) === toLower(value)
- ) ||
- t('message.entity-already-exists', {
- entity: t('label.name'),
- }),
- }
- )}
- {renderTextField(
- 'displayName',
- t('label.display-name'),
- t('message.enter-display-name'),
- 'display-name',
- {
- ...DISPLAY_NAME_FIELD_REGISTER_RULES,
- validate: (value) =>
- Boolean(trim(value)) ||
- t('message.field-text-is-required', {
- fieldText: t('label.display-name'),
- }),
- required: t('message.field-text-is-required', {
+
+ {renderTextField(
+ 'name',
+ t('label.name'),
+ t('label.enter-entity', { entity: t('label.name') }),
+ 'name',
+ {
+ ...NAME_FIELD_REGISTER_RULES,
+ validate: (value) =>
+ !allTeam.some((team) => toLower(team.name) === toLower(value)) ||
+ t('message.entity-already-exists', {
+ entity: t('label.name'),
+ }),
+ }
+ )}
+ {renderTextField(
+ 'displayName',
+ t('label.display-name'),
+ t('message.enter-display-name'),
+ 'display-name',
+ {
+ ...DISPLAY_NAME_FIELD_REGISTER_RULES,
+ validate: (value) =>
+ Boolean(trim(value)) ||
+ t('message.field-text-is-required', {
fieldText: t('label.display-name'),
}),
- }
+ required: t('message.field-text-is-required', {
+ fieldText: t('label.display-name'),
+ }),
+ }
+ )}
+ {renderTextField(
+ 'email',
+ t('label.email'),
+ t('label.enter-entity', { entity: t('label.email-lowercase') }),
+ 'email',
+ {
+ pattern: {
+ value: EMAIL_REG_EX,
+ message: t('message.entity-is-not-valid', {
+ entity: t('label.email'),
+ }),
+ },
+ }
+ )}
+
+ {({ field }) => (
+
)}
- {renderTextField(
- 'email',
- t('label.email'),
- t('label.enter-entity', { entity: t('label.email-lowercase') }),
- 'email',
+
+
- {({ field }) => (
-
- )}
-
-
+
+
+
-
-
-
-
-
- {({ field }) => (
-
-
-
-
+
+
+ {({ field }) => (
+
+
+
+
+ Promise.resolve(
field.onChange(
domains === undefined
? undefined
: ([] as EntityReference[]).concat(domains)
)
- }>
-
-
-
- {selectedDomain.length > 0 && (
-
+
- )}
+
- )}
-
-
-
+ {selectedDomain.length > 0 && (
+
+ )}
+
+ )}
+
+
);
};
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 ec267bf39319..a8526f271a21 100644
--- a/openmetadata-ui/src/main/resources/ui/src/styles/app.less
+++ b/openmetadata-ui/src/main/resources/ui/src/styles/app.less
@@ -38,7 +38,7 @@ body {
// Login video panel and card. Fixed brand illustration colours (sampled from
// the login videos) with no semantic token, named once so LoginClassBase uses
-// one class per surface. The dark panel matches the form side; the dark card
+// one class per surface. The dark panel matches Collate's (gray-modern-800); the dark card
// matches the dark video's frames, shown until it paints. Collate overrides
// these through LoginClassCollate.
:root {
@@ -60,8 +60,8 @@ body {
:root.dark-mode {
--login-panel-bg: linear-gradient(
- var(--tw-color-bg-primary),
- var(--tw-color-bg-primary)
+ var(--tw-color-gray-modern-800),
+ var(--tw-color-gray-modern-800)
);
--login-card-bg: linear-gradient(
180deg,
@@ -907,7 +907,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
@@ -942,7 +942,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 {
diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Alerts/AlertsUtil.test.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/Alerts/AlertsUtil.test.tsx
index 952912f2f4d8..71a5471bb454 100644
--- a/openmetadata-ui/src/main/resources/ui/src/utils/Alerts/AlertsUtil.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/utils/Alerts/AlertsUtil.test.tsx
@@ -58,7 +58,6 @@ import {
getFormattedDestinations,
getFunctionDisplayName,
getLabelsForEventDetails,
- listLengthValidator,
normalizeDestinationConfig,
} from './AlertsUtilPure';
@@ -145,12 +144,6 @@ describe('AlertsUtil tests', () => {
expect(icon).toStrictEqual();
});
- it('listLengthValidator should return error function', async () => {
- const error = listLengthValidator('name', 64);
-
- expect(typeof error).toBe('function');
- });
-
it('getAlertActionTypeDisplayName should return correct text for Slack', () => {
expect(getAlertActionTypeDisplayName(SubscriptionType.Slack)).toBe(
'label.slack'
diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Alerts/AlertsUtilPure.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Alerts/AlertsUtilPure.ts
index 9b9c7ad55db6..14af402c54a7 100644
--- a/openmetadata-ui/src/main/resources/ui/src/utils/Alerts/AlertsUtilPure.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/utils/Alerts/AlertsUtilPure.ts
@@ -11,7 +11,6 @@
* limitations under the License.
*/
-import type { RuleObject } from 'antd/lib/form';
import cryptoRandomString from 'crypto-random-string-with-promisify-polyfill';
import {
isBoolean,
@@ -61,25 +60,6 @@ export const getFunctionDisplayName = (func: string): string => {
return displayNameByFunction[func] ?? '';
};
-/**
- *
- * @param name Field name used to identify which field has error
- * @param minLengthRequired how many item should be there in the list
- * @returns If validation failed throws an error else resolve
- */
-export const listLengthValidator =
- (name: string, minLengthRequired = 1) =>
- async (_: RuleObject, list: T[]) => {
- if (!list || list.length < minLengthRequired) {
- throw new Error(
- t('message.length-validator-error', {
- length: minLengthRequired,
- field: name,
- })
- );
- }
- };
-
export const getAlertActionTypeDisplayName = (
alertActionType: SubscriptionType
) => {
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..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';
@@ -458,14 +459,102 @@ describe('CSV utils ClassBase', () => {
expect(editor).toBeDefined();
});
- it('should return editor for entityType* column', () => {
+ it('should commit the picked entity type and close the cell', async () => {
const editor = csvUtils.getEditor(
'entityType*',
EntityType.TABLE,
multipleOwner
);
+ const onRowChange = jest.fn();
+ const onClose = jest.fn();
- expect(editor).toBeDefined();
+ if (!editor) {
+ throw new Error('Expected entityType* editor to be defined');
+ }
+
+ render(
+ <>
+ {editor({
+ row: { 'entityType*': EntityType.DATABASE },
+ column: { key: 'entityType*' },
+ onRowChange,
+ onClose,
+ } as unknown as Parameters[0])}
+ >
+ );
+
+ fireEvent.click(
+ await screen.findByRole('option', { name: 'label.database-schema' })
+ );
+
+ expect(onRowChange).toHaveBeenCalledWith(
+ { 'entityType*': EntityType.DATABASE_SCHEMA },
+ true
+ );
+ expect(screen.queryByTestId('inline-save-btn')).not.toBeInTheDocument();
+ });
+
+ it('should end the enum edit when the list closes without a new pick', async () => {
+ const editor = csvUtils.getEditor(
+ 'entityType*',
+ EntityType.TABLE,
+ multipleOwner
+ );
+ const onRowChange = jest.fn();
+ const onClose = jest.fn();
+
+ if (!editor) {
+ throw new Error('Expected entityType* editor to be defined');
+ }
+
+ render(
+ <>
+ {editor({
+ row: { 'entityType*': EntityType.DATABASE },
+ column: { key: 'entityType*' },
+ onRowChange,
+ onClose,
+ } as unknown as Parameters[0])}
+ >
+ );
+
+ fireEvent.click(
+ await screen.findByRole('option', { name: 'label.database' })
+ );
+
+ expect(onRowChange).not.toHaveBeenCalled();
+ expect(onClose).toHaveBeenCalledWith(false);
+ });
+
+ 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();
+
+ if (!editor) {
+ throw new Error('Expected metricType editor to be defined');
+ }
+
+ 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', () => {
@@ -950,19 +1039,139 @@ 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();
});
+ 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();
+ const onClose = 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,
+ } as unknown as Parameters[0])}
+ >
+ );
+ });
+
+ 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'
+ )
+ );
+ 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(
+ { extension: 'status:Approved' },
+ true
+ );
+ });
+ });
+
+ 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 497bfe784468..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
@@ -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,
@@ -33,12 +34,13 @@ import {
Sliders02,
Tag01,
Type01,
+ XClose,
} 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 +1478,69 @@ 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);
- };
+ // 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, [])}
+ />
+ )}
+
+ );
+ }
+
+ const handleInserted = (key: Key) =>
+ handleUpdateDraft(customProperty.name, [
+ ...new Set([...selectedValues, String(key)]),
+ ]);
+
+ const handleCleared = (key: Key) =>
+ handleUpdateDraft(
+ customProperty.name,
+ selectedValues.filter((value) => value !== String(key))
+ );
return (
-