From c87bfa2ac827fbcc7235c710477fa84713f88e75 Mon Sep 17 00:00:00 2001 From: Shailesh Parmar Date: Mon, 16 Sep 2024 16:24:22 +0530 Subject: [PATCH 1/6] Feat: Introducing incident tab in table details page --- .../IncidentManager.component.tsx | 545 ++++++++++++++++++ .../IncidentManager.interface.ts | 18 + .../IncidentManager/IncidentManager.test.tsx | 138 +++++ .../resources/ui/src/enums/entity.enum.ts | 1 + .../ui/src/locale/languages/de-de.json | 1 + .../ui/src/locale/languages/en-us.json | 1 + .../ui/src/locale/languages/es-es.json | 1 + .../ui/src/locale/languages/fr-fr.json | 1 + .../ui/src/locale/languages/he-he.json | 1 + .../ui/src/locale/languages/ja-jp.json | 1 + .../ui/src/locale/languages/nl-nl.json | 1 + .../ui/src/locale/languages/pt-br.json | 1 + .../ui/src/locale/languages/ru-ru.json | 1 + .../ui/src/locale/languages/zh-cn.json | 1 + .../IncidentManagerPage.test.tsx | 110 +--- .../IncidentManager/IncidentManagerPage.tsx | 515 +---------------- .../TableDetailsPageV1/TableDetailsPageV1.tsx | 21 + 17 files changed, 744 insertions(+), 614 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.component.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.interface.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.test.tsx diff --git a/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.component.tsx new file mode 100644 index 000000000000..4e8480e97e67 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.component.tsx @@ -0,0 +1,545 @@ +/* + * 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 { Col, Row, Select, Space } from 'antd'; +import { DefaultOptionType } from 'antd/lib/select'; +import { ColumnsType } from 'antd/lib/table'; +import { AxiosError } from 'axios'; +import { compare } from 'fast-json-patch'; +import { isEqual, pick, startCase } from 'lodash'; +import { DateRangeObject } from 'Models'; +import QueryString from 'qs'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Link } from 'react-router-dom'; +import { WILD_CARD_CHAR } from '../../constants/char.constants'; +import { + getEntityDetailsPath, + PAGE_SIZE_BASE, + PAGE_SIZE_MEDIUM, +} from '../../constants/constants'; +import { PROFILER_FILTER_RANGE } from '../../constants/profiler.constant'; +import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider'; +import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; +import { EntityTabs, EntityType, FqnPart } from '../../enums/entity.enum'; +import { SearchIndex } from '../../enums/search.enum'; +import { EntityReference } from '../../generated/tests/testCase'; +import { + Assigned, + Severities, + TestCaseResolutionStatus, + TestCaseResolutionStatusTypes, +} from '../../generated/tests/testCaseResolutionStatus'; +import { usePaging } from '../../hooks/paging/usePaging'; +import { + SearchHitBody, + TestCaseSearchSource, +} from '../../interface/search.interface'; +import { TestCaseIncidentStatusData } from '../../pages/IncidentManager/IncidentManager.interface'; +import Assignees from '../../pages/TasksPage/shared/Assignees'; +import { Option } from '../../pages/TasksPage/TasksPage.interface'; +import { + getListTestCaseIncidentStatus, + TestCaseIncidentStatusParams, + updateTestCaseIncidentById, +} from '../../rest/incidentManagerAPI'; +import { getUserSuggestions } from '../../rest/miscAPI'; +import { searchQuery } from '../../rest/searchAPI'; +import { getUsers } from '../../rest/userAPI'; +import { + getNameFromFQN, + getPartialNameFromTableFQN, +} from '../../utils/CommonUtils'; +import { + formatDateTime, + getCurrentMillis, + getEpochMillisForPastDays, +} from '../../utils/date-time/DateTimeUtils'; +import { + getEntityName, + getEntityReferenceListFromEntities, +} from '../../utils/EntityUtils'; +import { getIncidentManagerDetailPagePath } from '../../utils/RouterUtils'; +import { showErrorToast } from '../../utils/ToastUtils'; +import { AsyncSelect } from '../common/AsyncSelect/AsyncSelect'; +import DatePickerMenu from '../common/DatePickerMenu/DatePickerMenu.component'; +import ErrorPlaceHolder from '../common/ErrorWithPlaceholder/ErrorPlaceHolder'; +import FilterTablePlaceHolder from '../common/ErrorWithPlaceholder/FilterTablePlaceHolder'; +import NextPrevious from '../common/NextPrevious/NextPrevious'; +import { PagingHandlerParams } from '../common/NextPrevious/NextPrevious.interface'; +import { OwnerLabel } from '../common/OwnerLabel/OwnerLabel.component'; +import Table from '../common/Table/Table'; +import { TableProfilerTab } from '../Database/Profiler/ProfilerDashboard/profilerDashboard.interface'; +import Severity from '../DataQuality/IncidentManager/Severity/Severity.component'; +import TestCaseIncidentManagerStatus from '../DataQuality/IncidentManager/TestCaseStatus/TestCaseIncidentManagerStatus.component'; +import { IncidentManagerProps } from './IncidentManager.interface'; + +const IncidentManager = ({ isIncidentPage = true }: IncidentManagerProps) => { + const defaultRange = useMemo( + () => ({ + key: 'last30days', + title: PROFILER_FILTER_RANGE.last30days.title, + }), + [] + ); + const [testCaseListData, setTestCaseListData] = + useState({ + data: [], + isLoading: true, + }); + const [filters, setFilters] = useState({ + startTs: getEpochMillisForPastDays(PROFILER_FILTER_RANGE.last30days.days), + endTs: getCurrentMillis(), + }); + const [users, setUsers] = useState<{ + options: Option[]; + selected: Option[]; + }>({ + options: [], + selected: [], + }); + const [testCaseInitialOptions, setTestCaseInitialOptions] = + useState(); + const [initialAssignees, setInitialAssignees] = useState( + [] + ); + + const { t } = useTranslation(); + + const { permissions } = usePermissionProvider(); + const { testCase: testCasePermission } = permissions; + + const { + paging, + pageSize, + currentPage, + showPagination, + handlePageChange, + handlePagingChange, + handlePageSizeChange, + } = usePaging(); + + const fetchTestCaseIncidents = useCallback( + async (params: TestCaseIncidentStatusParams) => { + setTestCaseListData((prev) => ({ ...prev, isLoading: true })); + try { + const { data, paging } = await getListTestCaseIncidentStatus({ + limit: pageSize, + latest: true, + ...params, + }); + const assigneeOptions = data.reduce((acc, curr) => { + const assignee = curr.testCaseResolutionStatusDetails?.assignee; + const isExist = acc.some((item) => item.value === assignee?.id); + + if (assignee && !isExist) { + acc.push({ + label: getEntityName(assignee), + value: assignee.id, + type: assignee.type, + name: assignee.name, + }); + } + + return acc; + }, [] as Option[]); + setUsers((pre) => ({ + ...pre, + options: assigneeOptions, + })); + setTestCaseListData((prev) => ({ ...prev, data: data })); + handlePagingChange(paging); + } catch (error) { + showErrorToast(error as AxiosError); + } finally { + setTestCaseListData((prev) => ({ ...prev, isLoading: false })); + } + }, + [pageSize, setTestCaseListData] + ); + + const handlePagingClick = ({ + cursorType, + currentPage, + }: PagingHandlerParams) => { + if (cursorType) { + fetchTestCaseIncidents({ + ...filters, + [cursorType]: paging?.[cursorType], + offset: paging?.[cursorType], + }); + } + handlePageChange(currentPage); + }; + + const pagingData = useMemo( + () => ({ + paging, + currentPage, + pagingHandler: handlePagingClick, + pageSize, + onShowSizeChange: handlePageSizeChange, + }), + [paging, currentPage, handlePagingClick, pageSize, handlePageSizeChange] + ); + + const handleSeveritySubmit = async ( + severity: Severities, + record: TestCaseResolutionStatus + ) => { + const updatedData = { ...record, severity }; + const patch = compare(record, updatedData); + try { + await updateTestCaseIncidentById(record.id ?? '', patch); + + setTestCaseListData((prev) => { + const testCaseList = prev.data.map((item) => { + if (item.id === updatedData.id) { + return updatedData; + } + + return item; + }); + + return { + ...prev, + data: testCaseList, + }; + }); + } catch (error) { + showErrorToast(error as AxiosError); + } + }; + + const fetchUserFilterOptions = async (query: string) => { + if (!query) { + return; + } + try { + const res = await getUserSuggestions(query, true); + const hits = res.data.suggest['metadata-suggest'][0]['options']; + const suggestOptions = hits.map((hit) => ({ + label: getEntityName(hit._source), + value: hit._id, + type: hit._source.entityType, + name: hit._source.name, + })); + + setUsers((pre) => ({ ...pre, options: suggestOptions })); + } catch (error) { + setUsers((pre) => ({ ...pre, options: [] })); + } + }; + + const handleAssigneeChange = (value?: Option[]) => { + setUsers((pre) => ({ ...pre, selected: value ?? [] })); + setFilters((pre) => ({ + ...pre, + assignee: value ? value[0]?.name : value, + })); + }; + + const handleDateRangeChange = (value: DateRangeObject) => { + const updatedFilter = pick(value, ['startTs', 'endTs']); + const existingFilters = pick(filters, ['startTs', 'endTs']); + + if (!isEqual(existingFilters, updatedFilter)) { + setFilters((pre) => ({ ...pre, ...updatedFilter })); + } + }; + + const handleStatusSubmit = (value: TestCaseResolutionStatus) => { + setTestCaseListData((prev) => { + const testCaseList = prev.data.map((item) => { + if (item.stateId === value.stateId) { + return value; + } + + return item; + }); + + return { + ...prev, + data: testCaseList, + }; + }); + }; + + const searchTestCases = async (searchValue = WILD_CARD_CHAR) => { + try { + const response = await searchQuery({ + pageNumber: 1, + pageSize: PAGE_SIZE_BASE, + searchIndex: SearchIndex.TEST_CASE, + query: searchValue, + fetchSource: true, + includeFields: ['name', 'displayName', 'fullyQualifiedName'], + }); + + return ( + response.hits.hits as SearchHitBody< + SearchIndex.TEST_CASE, + TestCaseSearchSource + >[] + ).map((hit) => ({ + label: getEntityName(hit._source), + value: hit._source.fullyQualifiedName, + })); + } catch (error) { + return []; + } + }; + + const getInitialOptions = async () => { + try { + const option = await searchTestCases(); + setTestCaseInitialOptions(option); + } catch (error) { + setTestCaseInitialOptions([]); + } + }; + useEffect(() => { + getInitialOptions(); + }, []); + + const fetchInitialAssign = useCallback(async () => { + try { + const { data } = await getUsers({ + limit: PAGE_SIZE_MEDIUM, + + isBot: false, + }); + const filterData = getEntityReferenceListFromEntities( + data, + EntityType.USER + ); + setInitialAssignees(filterData); + } catch (error) { + setInitialAssignees([]); + } + }, []); + + useEffect(() => { + // fetch users once and store in state + fetchInitialAssign(); + }, []); + + useEffect(() => { + if (testCasePermission?.ViewAll || testCasePermission?.ViewBasic) { + fetchTestCaseIncidents(filters); + } else { + setTestCaseListData((prev) => ({ ...prev, isLoading: false })); + } + }, [testCasePermission, pageSize, filters]); + + const columns: ColumnsType = useMemo( + () => [ + { + title: t('label.test-case-name'), + dataIndex: 'name', + key: 'name', + width: 300, + fixed: 'left', + render: (_, record) => { + return ( + + {getEntityName(record.testCaseReference)} + + ); + }, + }, + ...(isIncidentPage + ? [ + { + title: t('label.table'), + dataIndex: 'testCaseReference', + key: 'testCaseReference', + width: 150, + render: (value: EntityReference) => { + const tableFqn = getPartialNameFromTableFQN( + value.fullyQualifiedName ?? '', + [ + FqnPart.Service, + FqnPart.Database, + FqnPart.Schema, + FqnPart.Table, + ], + '.' + ); + + return ( + e.stopPropagation()}> + {getNameFromFQN(tableFqn) ?? value.fullyQualifiedName} + + ); + }, + }, + ] + : []), + { + title: t('label.execution-time'), + dataIndex: 'timestamp', + key: 'timestamp', + width: 150, + render: (value: number) => (value ? formatDateTime(value) : '--'), + }, + { + title: t('label.status'), + dataIndex: 'testCaseResolutionStatusType', + key: 'testCaseResolutionStatusType', + width: 100, + render: (_, record: TestCaseResolutionStatus) => ( + + ), + }, + { + title: t('label.severity'), + dataIndex: 'severity', + key: 'severity', + width: 150, + render: (value: Severities, record: TestCaseResolutionStatus) => { + return ( + handleSeveritySubmit(severity, record)} + /> + ); + }, + }, + { + title: t('label.assignee'), + dataIndex: 'testCaseResolutionStatusDetails', + key: 'testCaseResolutionStatusDetails', + width: 150, + render: (value?: Assigned) => ( + + ), + }, + ], + [testCaseListData.data, initialAssignees] + ); + + if (!testCasePermission?.ViewAll && !testCasePermission?.ViewBasic) { + return ; + } + + return ( + + + + fetchUserFilterOptions(query)} + /> + + + setFilters((pre) => ({ + ...pre, + testCaseFQN: value, + })) + } + /> + + + + + + + ), + }} + pagination={false} + rowKey="id" + size="small" + /> + + + {pagingData && showPagination && ( + + + + )} + + ); +}; + +export default IncidentManager; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.interface.ts new file mode 100644 index 000000000000..b85c79c2332c --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.interface.ts @@ -0,0 +1,18 @@ +/* + * 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 { Table } from '../../generated/entity/data/table'; + +export interface IncidentManagerProps { + isIncidentPage?: boolean; + tableDetails?: Table; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.test.tsx new file mode 100644 index 000000000000..34023890128b --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.test.tsx @@ -0,0 +1,138 @@ +/* + * Copyright 2023 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 React from 'react'; +import { getListTestCaseIncidentStatus } from '../../rest/incidentManagerAPI'; +import IncidentManager from './IncidentManager.component'; + +jest.mock('../common/NextPrevious/NextPrevious', () => { + return jest.fn().mockImplementation(() =>
NextPrevious.component
); +}); +jest.mock('../common/DatePickerMenu/DatePickerMenu.component', () => { + return jest.fn().mockImplementation(({ handleDateRangeChange }) => ( +
+

DatePickerMenu.component

+ +
+ )); +}); + +jest.mock('../../pages/TasksPage/shared/Assignees', () => { + return jest.fn().mockImplementation(() =>
Assignees.component
); +}); +jest.mock('../common/AsyncSelect/AsyncSelect', () => ({ + AsyncSelect: jest + .fn() + .mockImplementation(() =>
AsyncSelect.component
), +})); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + Link: jest.fn().mockImplementation(() =>
Link
), +})); +jest.mock('../../context/PermissionProvider/PermissionProvider', () => ({ + usePermissionProvider: jest.fn().mockReturnValue({ + permissions: { + testCase: { + Create: true, + Delete: true, + ViewAll: true, + EditAll: true, + EditDescription: true, + EditDisplayName: true, + EditCustomFields: true, + }, + }, + }), +})); +jest.mock('../../hooks/paging/usePaging', () => ({ + usePaging: jest.fn().mockReturnValue({ + currentPage: 1, + showPagination: true, + pageSize: 10, + handlePageChange: jest.fn(), + handlePagingChange: jest.fn(), + handlePageSizeChange: jest.fn(), + }), +})); +jest.mock('../../rest/incidentManagerAPI', () => ({ + getListTestCaseIncidentStatus: jest + .fn() + .mockImplementation(() => Promise.resolve({ data: [] })), + updateTestCaseIncidentById: jest.fn(), +})); +jest.mock('../../rest/searchAPI', () => ({ + searchQuery: jest + .fn() + .mockImplementation(() => Promise.resolve({ hits: { hits: [] } })), +})); + +describe('IncidentManagerPage', () => { + it('should render component', async () => { + render(); + + expect(await screen.findByTestId('status-select')).toBeInTheDocument(); + expect( + await screen.findByTestId('test-case-incident-manager-table') + ).toBeInTheDocument(); + expect(await screen.findByText('Assignees.component')).toBeInTheDocument(); + expect( + await screen.findByText('AsyncSelect.component') + ).toBeInTheDocument(); + expect( + await screen.findByText('DatePickerMenu.component') + ).toBeInTheDocument(); + expect( + await screen.findByText('NextPrevious.component') + ).toBeInTheDocument(); + }); + + it('Incident should be fetch with updated time', async () => { + const mockGetListTestCaseIncidentStatus = + getListTestCaseIncidentStatus as jest.Mock; + render(); + + const timeFilterButton = await screen.findByTestId('time-filter'); + + act(() => { + fireEvent.click(timeFilterButton); + }); + + expect(mockGetListTestCaseIncidentStatus).toHaveBeenCalledWith({ + endTs: 1710161424255, + latest: true, + limit: 10, + startTs: 1709556624254, + }); + }); + + it('Should not ender table column if isIncidentManager is false', () => { + render(); + + expect(screen.queryByText('label.table')).not.toBeInTheDocument(); + }); + + it('Should render table column if isIncidentManager is true', () => { + render(); + + expect(screen.getByText('label.table')).toBeInTheDocument(); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/entity.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/entity.enum.ts index dc50c0c38f90..2cb834508a36 100644 --- a/openmetadata-ui/src/main/resources/ui/src/enums/entity.enum.ts +++ b/openmetadata-ui/src/main/resources/ui/src/enums/entity.enum.ts @@ -203,6 +203,7 @@ export enum EntityTabs { API_COLLECTION = 'apiCollection', API_ENDPOINT = 'apiEndpoint', OVERVIEW = 'overview', + INCIDENT = 'incident', } export enum EntityAction { diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json index 9c5b80a34e99..ff114fb97766 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json @@ -575,6 +575,7 @@ "inactive-announcement-plural": "Inaktive Ankündigungen", "incident": "Incident", "incident-manager": "Incident Manager", + "incident-plural": "Incidents", "incident-status": "Incident Status", "include": "Einschließen", "include-entity": "{{entity}} einschließen", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json index 519815e095e1..43db8fdd5378 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json @@ -575,6 +575,7 @@ "inactive-announcement-plural": "Inactive Announcements", "incident": "Incident", "incident-manager": "Incident Manager", + "incident-plural": "Incidents", "incident-status": "Incident Status", "include": "Include", "include-entity": "Include {{entity}}", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json index df27481eeb84..40cc501822e2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json @@ -575,6 +575,7 @@ "inactive-announcement-plural": "Anuncios inactivos", "incident": "Incidente", "incident-manager": "Gestor de Incidentes", + "incident-plural": "Incidents", "incident-status": "Estado del Incidente", "include": "Incluir", "include-entity": "Incluir {{entity}}", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json index 44ee17b8cc62..84b762028bfa 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json @@ -575,6 +575,7 @@ "inactive-announcement-plural": "Annonces Inactives", "incident": "Incident", "incident-manager": "Gestionnaire d'Incidents", + "incident-plural": "Incidents", "incident-status": "Statut de l'Incident", "include": "Inclure", "include-entity": "Inclure {{entity}}", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json index c5c64a260910..0cae11b58e3f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json @@ -575,6 +575,7 @@ "inactive-announcement-plural": "הכרזות לא פעילות", "incident": "Incident", "incident-manager": "Incident Manager", + "incident-plural": "Incidents", "incident-status": "Incident Status", "include": "כלול", "include-entity": "כלול {{entity}}", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json index 5608cb129a19..26040bf0f31f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json @@ -575,6 +575,7 @@ "inactive-announcement-plural": "お知らせを非表示にする", "incident": "Incident", "incident-manager": "Incident Manager", + "incident-plural": "Incidents", "incident-status": "Incident Status", "include": "Include", "include-entity": "{{entity}}を含める", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json index 90c39302078d..709d19276ab6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json @@ -575,6 +575,7 @@ "inactive-announcement-plural": "Inactieve aankondigingen", "incident": "Incident", "incident-manager": "Incidentmanager", + "incident-plural": "Incidents", "incident-status": "Incidentstatus", "include": "Inclusief", "include-entity": "{{entity}} opnemen", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json index e374fac7735c..1a2195d420ea 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json @@ -575,6 +575,7 @@ "inactive-announcement-plural": "Anúncios Inativos", "incident": "Incidente", "incident-manager": "Gestão de Incidente", + "incident-plural": "Incidents", "incident-status": "Status do Incidente", "include": "Incluir", "include-entity": "Incluir {{entity}}", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json index fb8871c2c6bd..21941c203390 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json @@ -575,6 +575,7 @@ "inactive-announcement-plural": "Неактивные объявления", "incident": "Incident", "incident-manager": "Incident Manager", + "incident-plural": "Incidents", "incident-status": "Incident Status", "include": "Включать", "include-entity": "Включить {{entity}}", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json index 83b515660471..3ff3d5c75a99 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json @@ -575,6 +575,7 @@ "inactive-announcement-plural": "不活动的公告", "incident": "事件", "incident-manager": "事件管理", + "incident-plural": "Incidents", "incident-status": "事件状态", "include": "包括", "include-entity": "包括{{entity}}", diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/IncidentManager/IncidentManagerPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/IncidentManager/IncidentManagerPage.test.tsx index 1dc858df84b0..7e02307770d2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/IncidentManager/IncidentManagerPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/IncidentManager/IncidentManagerPage.test.tsx @@ -10,85 +10,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { act, fireEvent, render, screen } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import React from 'react'; -import { getListTestCaseIncidentStatus } from '../../rest/incidentManagerAPI'; import IncidentManagerPage from './IncidentManagerPage'; -jest.mock('../../components/common/NextPrevious/NextPrevious', () => { - return jest.fn().mockImplementation(() =>
NextPrevious.component
); -}); -jest.mock( - '../../components/common/DatePickerMenu/DatePickerMenu.component', - () => { - return jest.fn().mockImplementation(({ handleDateRangeChange }) => ( -
-

DatePickerMenu.component

- -
- )); - } -); jest.mock('../../components/PageLayoutV1/PageLayoutV1', () => { return jest.fn().mockImplementation(({ children }) =>
{children}
); }); -jest.mock('../TasksPage/shared/Assignees', () => { - return jest.fn().mockImplementation(() =>
Assignees.component
); -}); -jest.mock('../../components/common/AsyncSelect/AsyncSelect', () => ({ - AsyncSelect: jest - .fn() - .mockImplementation(() =>
AsyncSelect.component
), -})); -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - Link: jest.fn().mockImplementation(() =>
Link
), -})); -jest.mock('../../context/PermissionProvider/PermissionProvider', () => ({ - usePermissionProvider: jest.fn().mockReturnValue({ - permissions: { - testCase: { - Create: true, - Delete: true, - ViewAll: true, - EditAll: true, - EditDescription: true, - EditDisplayName: true, - EditCustomFields: true, - }, - }, - }), -})); -jest.mock('../../hooks/paging/usePaging', () => ({ - usePaging: jest.fn().mockReturnValue({ - currentPage: 1, - showPagination: true, - pageSize: 10, - handlePageChange: jest.fn(), - handlePagingChange: jest.fn(), - handlePageSizeChange: jest.fn(), - }), -})); -jest.mock('../../rest/incidentManagerAPI', () => ({ - getListTestCaseIncidentStatus: jest +jest.mock('../../components/IncidentManager/IncidentManager.component', () => { + return jest .fn() - .mockImplementation(() => Promise.resolve({ data: [] })), - updateTestCaseIncidentById: jest.fn(), -})); -jest.mock('../../rest/searchAPI', () => ({ - searchQuery: jest - .fn() - .mockImplementation(() => Promise.resolve({ hits: { hits: [] } })), -})); + .mockImplementation(() =>
IncidentManager.component
); +}); describe('IncidentManagerPage', () => { it('should render component', async () => { @@ -96,38 +29,9 @@ describe('IncidentManagerPage', () => { expect(await screen.findByTestId('page-title')).toBeInTheDocument(); expect(await screen.findByTestId('page-sub-title')).toBeInTheDocument(); - expect(await screen.findByTestId('status-select')).toBeInTheDocument(); - expect( - await screen.findByTestId('test-case-incident-manager-table') - ).toBeInTheDocument(); - expect(await screen.findByText('Assignees.component')).toBeInTheDocument(); - expect( - await screen.findByText('AsyncSelect.component') - ).toBeInTheDocument(); - expect( - await screen.findByText('DatePickerMenu.component') - ).toBeInTheDocument(); + expect( - await screen.findByText('NextPrevious.component') + await screen.findByText('IncidentManager.component') ).toBeInTheDocument(); }); - - it('Incident should be fetch with updated time', async () => { - const mockGetListTestCaseIncidentStatus = - getListTestCaseIncidentStatus as jest.Mock; - render(); - - const timeFilterButton = await screen.findByTestId('time-filter'); - - act(() => { - fireEvent.click(timeFilterButton); - }); - - expect(mockGetListTestCaseIncidentStatus).toHaveBeenCalledWith({ - endTs: 1710161424255, - latest: true, - limit: 10, - startTs: 1709556624254, - }); - }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/IncidentManager/IncidentManagerPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/IncidentManager/IncidentManagerPage.tsx index 55106225b68a..d340f727f62f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/IncidentManager/IncidentManagerPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/IncidentManager/IncidentManagerPage.tsx @@ -10,444 +10,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Col, Row, Select, Space, Table, Typography } from 'antd'; -import { DefaultOptionType } from 'antd/lib/select'; -import { ColumnsType } from 'antd/lib/table'; -import { AxiosError } from 'axios'; -import { compare } from 'fast-json-patch'; -import { isEqual, pick, startCase } from 'lodash'; -import { DateRangeObject } from 'Models'; -import QueryString from 'qs'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Link } from 'react-router-dom'; -import { AsyncSelect } from '../../components/common/AsyncSelect/AsyncSelect'; -import DatePickerMenu from '../../components/common/DatePickerMenu/DatePickerMenu.component'; -import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; -import FilterTablePlaceHolder from '../../components/common/ErrorWithPlaceholder/FilterTablePlaceHolder'; -import NextPrevious from '../../components/common/NextPrevious/NextPrevious'; -import { PagingHandlerParams } from '../../components/common/NextPrevious/NextPrevious.interface'; -import { OwnerLabel } from '../../components/common/OwnerLabel/OwnerLabel.component'; -import { TableProfilerTab } from '../../components/Database/Profiler/ProfilerDashboard/profilerDashboard.interface'; -import Severity from '../../components/DataQuality/IncidentManager/Severity/Severity.component'; -import TestCaseIncidentManagerStatus from '../../components/DataQuality/IncidentManager/TestCaseStatus/TestCaseIncidentManagerStatus.component'; +import { Col, Row, Typography } from 'antd'; +import React from 'react'; +import IncidentManager from '../../components/IncidentManager/IncidentManager.component'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; -import { WILD_CARD_CHAR } from '../../constants/char.constants'; -import { - getEntityDetailsPath, - PAGE_SIZE_BASE, - PAGE_SIZE_MEDIUM, -} from '../../constants/constants'; import { PAGE_HEADERS } from '../../constants/PageHeaders.constant'; -import { PROFILER_FILTER_RANGE } from '../../constants/profiler.constant'; -import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider'; -import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; -import { EntityTabs, EntityType, FqnPart } from '../../enums/entity.enum'; -import { SearchIndex } from '../../enums/search.enum'; -import { EntityReference } from '../../generated/entity/type'; -import { - Assigned, - Severities, - TestCaseResolutionStatus, - TestCaseResolutionStatusTypes, -} from '../../generated/tests/testCaseResolutionStatus'; -import { usePaging } from '../../hooks/paging/usePaging'; -import { - SearchHitBody, - TestCaseSearchSource, -} from '../../interface/search.interface'; -import { - getListTestCaseIncidentStatus, - TestCaseIncidentStatusParams, - updateTestCaseIncidentById, -} from '../../rest/incidentManagerAPI'; -import { getUserSuggestions } from '../../rest/miscAPI'; -import { searchQuery } from '../../rest/searchAPI'; -import { getUsers } from '../../rest/userAPI'; -import { - getNameFromFQN, - getPartialNameFromTableFQN, -} from '../../utils/CommonUtils'; -import { - formatDateTime, - getCurrentMillis, - getEpochMillisForPastDays, -} from '../../utils/date-time/DateTimeUtils'; -import { - getEntityName, - getEntityReferenceListFromEntities, -} from '../../utils/EntityUtils'; -import { getIncidentManagerDetailPagePath } from '../../utils/RouterUtils'; -import { showErrorToast } from '../../utils/ToastUtils'; -import Assignees from '../TasksPage/shared/Assignees'; -import { Option } from '../TasksPage/TasksPage.interface'; -import { TestCaseIncidentStatusData } from './IncidentManager.interface'; const IncidentManagerPage = () => { - const defaultRange = useMemo( - () => ({ - key: 'last30days', - title: PROFILER_FILTER_RANGE.last30days.title, - }), - [] - ); - const [testCaseListData, setTestCaseListData] = - useState({ - data: [], - isLoading: true, - }); - const [filters, setFilters] = useState({ - startTs: getEpochMillisForPastDays(PROFILER_FILTER_RANGE.last30days.days), - endTs: getCurrentMillis(), - }); - const [users, setUsers] = useState<{ - options: Option[]; - selected: Option[]; - }>({ - options: [], - selected: [], - }); - const [testCaseInitialOptions, setTestCaseInitialOptions] = - useState(); - const [initialAssignees, setInitialAssignees] = useState( - [] - ); - - const { t } = useTranslation(); - - const { permissions } = usePermissionProvider(); - const { testCase: testCasePermission } = permissions; - - const { - paging, - pageSize, - currentPage, - showPagination, - handlePageChange, - handlePagingChange, - handlePageSizeChange, - } = usePaging(); - - const fetchTestCaseIncidents = useCallback( - async (params: TestCaseIncidentStatusParams) => { - setTestCaseListData((prev) => ({ ...prev, isLoading: true })); - try { - const { data, paging } = await getListTestCaseIncidentStatus({ - limit: pageSize, - latest: true, - ...params, - }); - const assigneeOptions = data.reduce((acc, curr) => { - const assignee = curr.testCaseResolutionStatusDetails?.assignee; - const isExist = acc.some((item) => item.value === assignee?.id); - - if (assignee && !isExist) { - acc.push({ - label: getEntityName(assignee), - value: assignee.id, - type: assignee.type, - name: assignee.name, - }); - } - - return acc; - }, [] as Option[]); - setUsers((pre) => ({ - ...pre, - options: assigneeOptions, - })); - setTestCaseListData((prev) => ({ ...prev, data: data })); - handlePagingChange(paging); - } catch (error) { - showErrorToast(error as AxiosError); - } finally { - setTestCaseListData((prev) => ({ ...prev, isLoading: false })); - } - }, - [pageSize, setTestCaseListData] - ); - - const handlePagingClick = ({ - cursorType, - currentPage, - }: PagingHandlerParams) => { - if (cursorType) { - fetchTestCaseIncidents({ - ...filters, - [cursorType]: paging?.[cursorType], - offset: paging?.[cursorType], - }); - } - handlePageChange(currentPage); - }; - - const pagingData = useMemo( - () => ({ - paging, - currentPage, - pagingHandler: handlePagingClick, - pageSize, - onShowSizeChange: handlePageSizeChange, - }), - [paging, currentPage, handlePagingClick, pageSize, handlePageSizeChange] - ); - - const handleSeveritySubmit = async ( - severity: Severities, - record: TestCaseResolutionStatus - ) => { - const updatedData = { ...record, severity }; - const patch = compare(record, updatedData); - try { - await updateTestCaseIncidentById(record.id ?? '', patch); - - setTestCaseListData((prev) => { - const testCaseList = prev.data.map((item) => { - if (item.id === updatedData.id) { - return updatedData; - } - - return item; - }); - - return { - ...prev, - data: testCaseList, - }; - }); - } catch (error) { - showErrorToast(error as AxiosError); - } - }; - - const fetchUserFilterOptions = async (query: string) => { - if (!query) { - return; - } - try { - const res = await getUserSuggestions(query, true); - const hits = res.data.suggest['metadata-suggest'][0]['options']; - const suggestOptions = hits.map((hit) => ({ - label: getEntityName(hit._source), - value: hit._id, - type: hit._source.entityType, - name: hit._source.name, - })); - - setUsers((pre) => ({ ...pre, options: suggestOptions })); - } catch (error) { - setUsers((pre) => ({ ...pre, options: [] })); - } - }; - - const handleAssigneeChange = (value?: Option[]) => { - setUsers((pre) => ({ ...pre, selected: value ?? [] })); - setFilters((pre) => ({ - ...pre, - assignee: value ? value[0]?.name : value, - })); - }; - - const handleDateRangeChange = (value: DateRangeObject) => { - const updatedFilter = pick(value, ['startTs', 'endTs']); - const existingFilters = pick(filters, ['startTs', 'endTs']); - - if (!isEqual(existingFilters, updatedFilter)) { - setFilters((pre) => ({ ...pre, ...updatedFilter })); - } - }; - - const handleStatusSubmit = (value: TestCaseResolutionStatus) => { - setTestCaseListData((prev) => { - const testCaseList = prev.data.map((item) => { - if (item.stateId === value.stateId) { - return value; - } - - return item; - }); - - return { - ...prev, - data: testCaseList, - }; - }); - }; - - const searchTestCases = async (searchValue = WILD_CARD_CHAR) => { - try { - const response = await searchQuery({ - pageNumber: 1, - pageSize: PAGE_SIZE_BASE, - searchIndex: SearchIndex.TEST_CASE, - query: searchValue, - fetchSource: true, - includeFields: ['name', 'displayName', 'fullyQualifiedName'], - }); - - return ( - response.hits.hits as SearchHitBody< - SearchIndex.TEST_CASE, - TestCaseSearchSource - >[] - ).map((hit) => ({ - label: getEntityName(hit._source), - value: hit._source.fullyQualifiedName, - })); - } catch (error) { - return []; - } - }; - - const getInitialOptions = async () => { - try { - const option = await searchTestCases(); - setTestCaseInitialOptions(option); - } catch (error) { - setTestCaseInitialOptions([]); - } - }; - useEffect(() => { - getInitialOptions(); - }, []); - - const fetchInitialAssign = useCallback(async () => { - try { - const { data } = await getUsers({ - limit: PAGE_SIZE_MEDIUM, - - isBot: false, - }); - const filterData = getEntityReferenceListFromEntities( - data, - EntityType.USER - ); - setInitialAssignees(filterData); - } catch (error) { - setInitialAssignees([]); - } - }, []); - - useEffect(() => { - // fetch users once and store in state - fetchInitialAssign(); - }, []); - - useEffect(() => { - if (testCasePermission?.ViewAll || testCasePermission?.ViewBasic) { - fetchTestCaseIncidents(filters); - } else { - setTestCaseListData((prev) => ({ ...prev, isLoading: false })); - } - }, [testCasePermission, pageSize, filters]); - - const columns: ColumnsType = useMemo( - () => [ - { - title: t('label.test-case-name'), - dataIndex: 'name', - key: 'name', - width: 300, - fixed: 'left', - render: (_, record) => { - return ( - - {getEntityName(record.testCaseReference)} - - ); - }, - }, - { - title: t('label.table'), - dataIndex: 'testCaseReference', - key: 'testCaseReference', - width: 150, - render: (value: EntityReference) => { - const tableFqn = getPartialNameFromTableFQN( - value.fullyQualifiedName ?? '', - [FqnPart.Service, FqnPart.Database, FqnPart.Schema, FqnPart.Table], - '.' - ); - - return ( - e.stopPropagation()}> - {getNameFromFQN(tableFqn) ?? value.fullyQualifiedName} - - ); - }, - }, - { - title: t('label.execution-time'), - dataIndex: 'timestamp', - key: 'timestamp', - width: 150, - render: (value: number) => (value ? formatDateTime(value) : '--'), - }, - { - title: t('label.status'), - dataIndex: 'testCaseResolutionStatusType', - key: 'testCaseResolutionStatusType', - width: 100, - render: (_, record: TestCaseResolutionStatus) => ( - - ), - }, - { - title: t('label.severity'), - dataIndex: 'severity', - key: 'severity', - width: 150, - render: (value: Severities, record: TestCaseResolutionStatus) => { - return ( - handleSeveritySubmit(severity, record)} - /> - ); - }, - }, - { - title: t('label.assignee'), - dataIndex: 'testCaseResolutionStatusDetails', - key: 'testCaseResolutionStatusDetails', - width: 150, - render: (value?: Assigned) => ( - - ), - }, - ], - [testCaseListData.data, initialAssignees] - ); - - if (!testCasePermission?.ViewAll && !testCasePermission?.ViewBasic) { - return ; - } - return ( @@ -465,85 +34,9 @@ const IncidentManagerPage = () => { -
- - fetchUserFilterOptions(query)} - /> - - - setFilters((pre) => ({ - ...pre, - testCaseFQN: value, - })) - } - /> - - - - -
- ), - }} - pagination={false} - rowKey="id" - size="small" - /> + - - {pagingData && showPagination && ( - - - - )} ); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx index cdd35874544d..90522d816c26 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx @@ -40,6 +40,7 @@ import SchemaTab from '../../components/Database/SchemaTab/SchemaTab.component'; import TableQueries from '../../components/Database/TableQueries/TableQueries'; import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface'; import EntityRightPanel from '../../components/Entity/EntityRightPanel/EntityRightPanel'; +import IncidentManager from '../../components/IncidentManager/IncidentManager.component'; import Lineage from '../../components/Lineage/Lineage.component'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; @@ -744,6 +745,26 @@ const TableDetailsPageV1: React.FC = () => { /> ), }, + { + label: ( + + ), + key: EntityTabs.INCIDENT, + children: + tablePermissions.ViewAll || tablePermissions.ViewTests ? ( +
+ +
+ ) : ( + + ), + }, { label: , key: EntityTabs.LINEAGE, From 6abf732ece62a4491ca586549095aa3f9f4ff286 Mon Sep 17 00:00:00 2001 From: Shailesh Parmar Date: Fri, 20 Sep 2024 15:22:14 +0530 Subject: [PATCH 2/6] use search api instead of user suggest --- .../IncidentManager/IncidentManager.component.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.component.tsx index 4e8480e97e67..742240d471fe 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.component.tsx @@ -52,7 +52,7 @@ import { TestCaseIncidentStatusParams, updateTestCaseIncidentById, } from '../../rest/incidentManagerAPI'; -import { getUserSuggestions } from '../../rest/miscAPI'; +import { getUserAndTeamSearch } from '../../rest/miscAPI'; import { searchQuery } from '../../rest/searchAPI'; import { getUsers } from '../../rest/userAPI'; import { @@ -225,11 +225,11 @@ const IncidentManager = ({ isIncidentPage = true }: IncidentManagerProps) => { return; } try { - const res = await getUserSuggestions(query, true); - const hits = res.data.suggest['metadata-suggest'][0]['options']; + const res = await getUserAndTeamSearch(query, true); + const hits = res.data.hits.hits; const suggestOptions = hits.map((hit) => ({ label: getEntityName(hit._source), - value: hit._id, + value: hit._id ?? '', type: hit._source.entityType, name: hit._source.name, })); From 1b3f0859984b74a30fa104a2bc837f2bd5cf60f2 Mon Sep 17 00:00:00 2001 From: Shailesh Parmar Date: Fri, 20 Sep 2024 18:48:20 +0530 Subject: [PATCH 3/6] added incident tab for entity details page --- .../IncidentManager/IncidentManager.component.tsx | 6 +++++- .../src/main/resources/ui/src/rest/incidentManagerAPI.ts | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.component.tsx index 742240d471fe..bf1983a648e8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.component.tsx @@ -83,7 +83,10 @@ import Severity from '../DataQuality/IncidentManager/Severity/Severity.component import TestCaseIncidentManagerStatus from '../DataQuality/IncidentManager/TestCaseStatus/TestCaseIncidentManagerStatus.component'; import { IncidentManagerProps } from './IncidentManager.interface'; -const IncidentManager = ({ isIncidentPage = true }: IncidentManagerProps) => { +const IncidentManager = ({ + isIncidentPage = true, + tableDetails, +}: IncidentManagerProps) => { const defaultRange = useMemo( () => ({ key: 'last30days', @@ -135,6 +138,7 @@ const IncidentManager = ({ isIncidentPage = true }: IncidentManagerProps) => { const { data, paging } = await getListTestCaseIncidentStatus({ limit: pageSize, latest: true, + originEntityFQN: tableDetails?.fullyQualifiedName, ...params, }); const assigneeOptions = data.reduce((acc, curr) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/incidentManagerAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/incidentManagerAPI.ts index 2ebff5041cce..bcb5bbc97aa6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/incidentManagerAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/incidentManagerAPI.ts @@ -29,6 +29,7 @@ export type TestCaseIncidentStatusParams = ListParams & { assignee?: string; testCaseFQN?: string; offset?: string; + originEntityFQN?: string; }; export const getListTestCaseIncidentStatus = async ({ From 05a2c77d535f25efafb2ad15036171ec20d53565 Mon Sep 17 00:00:00 2001 From: Shailesh Parmar Date: Sat, 21 Sep 2024 15:01:09 +0530 Subject: [PATCH 4/6] added incident count in explore side bar and lineage side bar --- .../TableSummary/TableSummary.component.tsx | 35 +++++++++++++++++-- .../IncidentManager.component.tsx | 12 ------- .../resources/ui/src/enums/entity.enum.ts | 2 +- .../TableDetailsPageV1/TableDetailsPageV1.tsx | 4 +-- .../resources/ui/src/utils/EntityUtils.tsx | 24 +++++++++++-- 5 files changed, 57 insertions(+), 20 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/TableSummary/TableSummary.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/TableSummary/TableSummary.component.tsx index d69df79ee1ee..083ef52485f6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/TableSummary/TableSummary.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/TableSummary/TableSummary.component.tsx @@ -23,6 +23,7 @@ import { import { useTranslation } from 'react-i18next'; import { ROUTES } from '../../../../constants/constants'; import { mockTablePermission } from '../../../../constants/mockTourData.constants'; +import { PROFILER_FILTER_RANGE } from '../../../../constants/profiler.constant'; import { usePermissionProvider } from '../../../../context/PermissionProvider/PermissionProvider'; import { OperationPermission, @@ -33,9 +34,14 @@ import { ExplorePageTabs } from '../../../../enums/Explore.enum'; import { Table } from '../../../../generated/entity/data/table'; import { TestSummary } from '../../../../generated/tests/testCase'; import useCustomLocation from '../../../../hooks/useCustomLocation/useCustomLocation'; +import { getListTestCaseIncidentStatus } from '../../../../rest/incidentManagerAPI'; import { getLatestTableProfileByFqn } from '../../../../rest/tableAPI'; import { getTestCaseExecutionSummary } from '../../../../rest/testAPI'; import { formTwoDigitNumber } from '../../../../utils/CommonUtils'; +import { + getCurrentMillis, + getEpochMillisForPastDays, +} from '../../../../utils/date-time/DateTimeUtils'; import { getFormattedEntityData, getSortedTagsWithHighlight, @@ -69,6 +75,7 @@ function TableSummary({ const { getEntityPermission } = usePermissionProvider(); const [profileData, setProfileData] = useState(); + const [incidentCount, setIncidentCount] = useState(0); const [testSuiteSummary, setTestSuiteSummary] = useState(); const [tablePermissions, setTablePermissions] = useState( DEFAULT_ENTITY_PERMISSION @@ -100,6 +107,26 @@ function TableSummary({ } }; + const fetchIncidentCount = async () => { + if (tableDetails?.fullyQualifiedName) { + try { + const { paging } = await getListTestCaseIncidentStatus({ + limit: 0, + latest: true, + originEntityFQN: tableDetails?.fullyQualifiedName, + startTs: getEpochMillisForPastDays( + PROFILER_FILTER_RANGE.last30days.days + ), + endTs: getCurrentMillis(), + }); + + setIncidentCount(paging.total); + } catch (error) { + setIncidentCount(0); + } + } + }; + const fetchProfilerData = useCallback(async () => { try { const { profile, tableConstraints } = await getLatestTableProfileByFqn( @@ -165,8 +192,11 @@ function TableSummary({ }, [tableDetails, testSuiteSummary, viewProfilerPermission]); const entityInfo = useMemo( - () => getEntityOverview(ExplorePageTabs.TABLES, tableDetails), - [tableDetails] + () => + getEntityOverview(ExplorePageTabs.TABLES, tableDetails, { + incidentCount, + }), + [tableDetails, incidentCount] ); const formattedColumnsData: BasicEntityInfo[] = useMemo( @@ -196,6 +226,7 @@ function TableSummary({ if (shouldFetchProfilerData) { fetchProfilerData(); fetchAllTests(); + fetchIncidentCount(); } } else { setTablePermissions(mockTablePermission as OperationPermission); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.component.tsx index bf1983a648e8..73ec1fc5039f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.component.tsx @@ -303,18 +303,6 @@ const IncidentManager = ({ } }; - const getInitialOptions = async () => { - try { - const option = await searchTestCases(); - setTestCaseInitialOptions(option); - } catch (error) { - setTestCaseInitialOptions([]); - } - }; - useEffect(() => { - getInitialOptions(); - }, []); - const fetchInitialAssign = useCallback(async () => { try { const { data } = await getUsers({ diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/entity.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/entity.enum.ts index 2cb834508a36..41ce4bffd6c3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/enums/entity.enum.ts +++ b/openmetadata-ui/src/main/resources/ui/src/enums/entity.enum.ts @@ -203,7 +203,7 @@ export enum EntityTabs { API_COLLECTION = 'apiCollection', API_ENDPOINT = 'apiEndpoint', OVERVIEW = 'overview', - INCIDENT = 'incident', + INCIDENTS = 'incidents', } export enum EntityAction { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx index 90522d816c26..5253827e1d34 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx @@ -748,11 +748,11 @@ const TableDetailsPageV1: React.FC = () => { { label: ( ), - key: EntityTabs.INCIDENT, + key: EntityTabs.INCIDENTS, children: tablePermissions.ViewAll || tablePermissions.ViewTests ? (
diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx index 2848bde4859e..e4e96703cd81 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx @@ -246,7 +246,10 @@ const getTableFieldsFromTableDetails = (tableDetails: Table) => { }; }; -const getTableOverview = (tableDetails: Table) => { +const getTableOverview = ( + tableDetails: Table, + additionalInfo?: Record +) => { const { fullyQualifiedName, owners, @@ -347,6 +350,20 @@ const getTableOverview = (tableDetails: Table) => { isLink: false, visible: [DRAWER_NAVIGATION_OPTIONS.lineage], }, + { + name: i18next.t('label.incident-plural'), + value: additionalInfo?.incidentCount ?? 0, + isLink: true, + url: getEntityDetailsPath( + EntityType.TABLE, + fullyQualifiedName ?? '', + EntityTabs.INCIDENTS + ), + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, ]; return overview; @@ -1046,11 +1063,12 @@ const getMetricOverview = (metric: Metric) => { export const getEntityOverview = ( type: string, - entityDetail: EntityUnion + entityDetail: EntityUnion, + additionalInfo?: Record ): Array => { switch (type) { case ExplorePageTabs.TABLES: { - return getTableOverview(entityDetail as Table); + return getTableOverview(entityDetail as Table, additionalInfo); } case ExplorePageTabs.PIPELINES: { From 699fb7389d57248a955e8468af0272ce6ff74e60 Mon Sep 17 00:00:00 2001 From: Shailesh Parmar Date: Mon, 23 Sep 2024 18:03:17 +0530 Subject: [PATCH 5/6] added test case for changes --- .../e2e/Features/IncidentManager.spec.ts | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/IncidentManager.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/IncidentManager.spec.ts index 120cfd0a7e91..e8d89b35df8c 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/IncidentManager.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/IncidentManager.spec.ts @@ -388,4 +388,155 @@ test.describe('Incident Manager', () => { ); }); }); + + test('Validate Incident Tab in Entity details page', async ({ page }) => { + const testCases = table1.testCasesResponseData; + await table1.visitEntityPage(page); + const incidentListResponse = page.waitForResponse( + `/api/v1/dataQuality/testCases/testCaseIncidentStatus?*originEntityFQN=${table1.entityResponseData?.['fullyQualifiedName']}*` + ); + + await page.click('[data-testid="incidents"]'); + await incidentListResponse; + + for (const testCase of testCases) { + await expect( + page.locator(`[data-testid="test-case-${testCase?.['name']}"]`) + ).toBeVisible(); + } + const lineageResponse = page.waitForResponse( + `/api/v1/lineage/getLineage?*fqn=${table1.entityResponseData?.['fullyQualifiedName']}*` + ); + const incidentCountResponse = page.waitForResponse( + `/api/v1/dataQuality/testCases/testCaseIncidentStatus?*originEntityFQN=${table1.entityResponseData?.['fullyQualifiedName']}*limit=0*` + ); + await page.click('[data-testid="lineage"]'); + await lineageResponse; + await incidentCountResponse; + + await page.waitForSelector("[role='dialog']", { state: 'visible' }); + + await expect(page.getByTestId('Incidents-label')).toBeVisible(); + await expect(page.getByTestId('Incidents-value')).toContainText('3'); + + const incidentResponse = page.waitForResponse( + `/api/v1/dataQuality/testCases/testCaseIncidentStatus?*originEntityFQN=${table1.entityResponseData?.['fullyQualifiedName']}*` + ); + await page.getByTestId('Incidents-value').click(); + await incidentResponse; + + for (const testCase of testCases) { + await expect( + page.locator(`[data-testid="test-case-${testCase?.['name']}"]`) + ).toBeVisible(); + } + }); + + test("Verify filters in Incident Manager's page", async ({ page }) => { + const assigneeTestCase = { + username: user1.data.email.split('@')[0].toLocaleLowerCase(), + userDisplayName: user1.getUserName(), + testCaseName: table1.testCasesResponseData[2]?.['name'], + }; + const testCase1 = table1.testCasesResponseData[0]?.['name']; + const incidentDetailsRes = page.waitForResponse( + '/api/v1/dataQuality/testCases/testCaseIncidentStatus?latest=true&startTs=*&endTs=*&limit=*' + ); + await sidebarClick(page, SidebarItem.INCIDENT_MANAGER); + await incidentDetailsRes; + + await page.click('[data-testid="select-assignee"]'); + const searchUserResponse = page.waitForResponse( + `/api/v1/search/query?q=*${assigneeTestCase.userDisplayName}*index=user_search_index*` + ); + await page + .getByTestId('select-assignee') + .locator('input') + .fill(assigneeTestCase.userDisplayName); + await searchUserResponse; + + const assigneeFilterRes = page.waitForResponse( + `/api/v1/dataQuality/testCases/testCaseIncidentStatus?*assignee=${assigneeTestCase.username}*` + ); + await page.click(`[data-testid="${assigneeTestCase.username}"]`); + await assigneeFilterRes; + + await expect( + page.locator(`[data-testid="test-case-${assigneeTestCase.testCaseName}"]`) + ).toBeVisible(); + await expect( + page.locator(`[data-testid="test-case-${testCase1}"]`) + ).not.toBeVisible(); + + const nonAssigneeFilterRes = page.waitForResponse( + '/api/v1/dataQuality/testCases/testCaseIncidentStatus?latest=true&startTs=*&endTs=*&limit=*' + ); + await page + .getByTestId('select-assignee') + .getByLabel('close-circle') + .click(); + await nonAssigneeFilterRes; + + await page.click(`[data-testid="status-select"]`); + const statusFilterRes = page.waitForResponse( + '/api/v1/dataQuality/testCases/testCaseIncidentStatus?*testCaseResolutionStatusType=Assigned*' + ); + await page.click(`[title="Assigned"]`); + await statusFilterRes; + + await expect( + page.locator(`[data-testid="test-case-${assigneeTestCase.testCaseName}"]`) + ).toBeVisible(); + await expect( + page.locator(`[data-testid="test-case-${testCase1}"]`) + ).not.toBeVisible(); + + const nonStatusFilterRes = page.waitForResponse( + '/api/v1/dataQuality/testCases/testCaseIncidentStatus?latest=true&startTs=*&endTs=*&limit=*' + ); + await page.getByTestId('status-select').getByLabel('close-circle').click(); + await nonStatusFilterRes; + + await page.click('[data-testid="test-case-select"]'); + const testCaseResponse = page.waitForResponse( + `/api/v1/search/query?q=${testCase1}*index=test_case_search_index*` + ); + await page.getByTestId('test-case-select').locator('input').fill(testCase1); + await testCaseResponse; + + const testCaseFilterRes = page.waitForResponse( + `/api/v1/dataQuality/testCases/testCaseIncidentStatus?*testCaseFQN=*${testCase1}*` + ); + await page.click(`[title="${testCase1}"]`); + await testCaseFilterRes; + + await expect( + page.locator(`[data-testid="test-case-${assigneeTestCase.testCaseName}"]`) + ).not.toBeVisible(); + await expect( + page.locator(`[data-testid="test-case-${testCase1}"]`) + ).toBeVisible(); + + const nonTestCaseFilterRes = page.waitForResponse( + '/api/v1/dataQuality/testCases/testCaseIncidentStatus?latest=true&startTs=*&endTs=*&limit=*' + ); + await page + .getByTestId('test-case-select') + .getByLabel('close-circle') + .click(); + await nonTestCaseFilterRes; + + await page.click('[data-testid="date-picker-menu"]'); + const timeSeriesFilterRes = page.waitForResponse( + '/api/v1/dataQuality/testCases/testCaseIncidentStatus?latest=true&startTs=*&endTs=*&limit=*' + ); + await page.getByRole('menuitem', { name: 'Yesterday' }).click(); + await timeSeriesFilterRes; + + for (const testCase of table1.testCasesResponseData) { + await expect( + page.locator(`[data-testid="test-case-${testCase?.['name']}"]`) + ).toBeVisible(); + } + }); }); From 535c9b298f322e5a1012064e4bb77c1af9b061c7 Mon Sep 17 00:00:00 2001 From: Shailesh Parmar Date: Mon, 23 Sep 2024 20:21:39 +0530 Subject: [PATCH 6/6] fixed unit test --- .../TableSummary/TableSummary.test.tsx | 12 +++++++++--- .../IncidentManager/IncidentManager.component.tsx | 5 +---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/TableSummary/TableSummary.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/TableSummary/TableSummary.test.tsx index a5ebf366fb73..60b485fd91b0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/TableSummary/TableSummary.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/TableSummary/TableSummary.test.tsx @@ -75,7 +75,9 @@ jest.mock('../../../../context/PermissionProvider/PermissionProvider', () => ({ describe('TableSummary component tests', () => { it('Component should render properly, when loaded in the Explore page.', async () => { await act(async () => { - render(); + render(, { + wrapper: MemoryRouter, + }); }); const profilerHeader = screen.getByTestId('profiler-header'); @@ -176,7 +178,9 @@ describe('TableSummary component tests', () => { ); await act(async () => { - render(); + render(, { + wrapper: MemoryRouter, + }); }); const testsPassedLabel = screen.getByTestId('test-passed'); @@ -209,7 +213,9 @@ describe('TableSummary component tests', () => { }) ); await act(async () => { - render(); + render(, { + wrapper: MemoryRouter, + }); }); const testsPassedValue = screen.getByTestId('test-passed-value'); const testsAbortedValue = screen.getByTestId('test-aborted-value'); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.component.tsx index 73ec1fc5039f..242bd8d9311a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.component.tsx @@ -11,7 +11,6 @@ * limitations under the License. */ import { Col, Row, Select, Space } from 'antd'; -import { DefaultOptionType } from 'antd/lib/select'; import { ColumnsType } from 'antd/lib/table'; import { AxiosError } from 'axios'; import { compare } from 'fast-json-patch'; @@ -110,8 +109,7 @@ const IncidentManager = ({ options: [], selected: [], }); - const [testCaseInitialOptions, setTestCaseInitialOptions] = - useState(); + const [initialAssignees, setInitialAssignees] = useState( [] ); @@ -486,7 +484,6 @@ const IncidentManager = ({ api={searchTestCases} className="w-min-20" data-testid="test-case-select" - options={testCaseInitialOptions} placeholder={t('label.test-case')} suffixIcon={undefined} onChange={(value) =>