From 9fa9b9fc8af6b42bb1032a289fb19a4caad41bf4 Mon Sep 17 00:00:00 2001 From: Pranita Date: Thu, 21 Nov 2024 14:57:34 +0530 Subject: [PATCH 1/8] feat: edit display name --- .../DatabaseSchemaTable.interface.ts | 1 + .../DatabaseSchemaTable.tsx | 121 +++++++++++++++++- .../EntityNameModal.interface.ts | 2 +- .../common/DisplayName/DisplayName.test.tsx | 106 +++++++++++++++ .../common/DisplayName/DisplayName.tsx | 121 ++++++++++++++++++ .../DatabaseSchemaPage/SchemaTablesTab.tsx | 77 ++++++++--- .../DatabaseVersionPage.tsx | 2 +- .../ServiceMainTabContent.tsx | 117 ++++++++++++++++- .../ServiceVersionMainTabContent.interface.ts | 1 + .../ServiceVersionMainTabContent.tsx | 12 +- .../src/utils/ServiceMainTabContentUtils.tsx | 39 +++--- 11 files changed, 553 insertions(+), 46 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.test.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.tsx diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable.interface.ts index 792196cb0c30..9038eef2b3c8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable.interface.ts @@ -13,4 +13,5 @@ export interface DatabaseSchemaTableProps { isDatabaseDeleted?: boolean; + isVersionPage?: boolean; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable.tsx index 36acb772229d..26b02d32a2ef 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable.tsx @@ -11,45 +11,69 @@ * limitations under the License. */ import { Col, Row, Switch, Typography } from 'antd'; +import { ColumnsType } from 'antd/lib/table'; import { AxiosError } from 'axios'; +import { compare } from 'fast-json-patch'; import { t } from 'i18next'; -import { isEmpty } from 'lodash'; +import { isEmpty, isUndefined } from 'lodash'; import QueryString from 'qs'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useHistory } from 'react-router-dom'; import { + getEntityDetailsPath, INITIAL_PAGING_VALUE, + NO_DATA_PLACEHOLDER, PAGE_SIZE, } from '../../../../constants/constants'; -import { TabSpecificField } from '../../../../enums/entity.enum'; +import { usePermissionProvider } from '../../../../context/PermissionProvider/PermissionProvider'; +import { EntityType, TabSpecificField } from '../../../../enums/entity.enum'; import { SearchIndex } from '../../../../enums/search.enum'; import { DatabaseSchema } from '../../../../generated/entity/data/databaseSchema'; +import { EntityReference } from '../../../../generated/entity/type'; +import { UsageDetails } from '../../../../generated/type/entityUsage'; import { Include } from '../../../../generated/type/include'; import { Paging } from '../../../../generated/type/paging'; import { usePaging } from '../../../../hooks/paging/usePaging'; import useCustomLocation from '../../../../hooks/useCustomLocation/useCustomLocation'; import { useFqn } from '../../../../hooks/useFqn'; -import { getDatabaseSchemas } from '../../../../rest/databaseAPI'; +import { + getDatabaseSchemas, + patchDatabaseSchemaDetails, +} from '../../../../rest/databaseAPI'; import { searchQuery } from '../../../../rest/searchAPI'; -import { schemaTableColumns } from '../../../../utils/Database/Database.util'; +import { getEntityName } from '../../../../utils/EntityUtils'; +import { getUsagePercentile } from '../../../../utils/TableUtils'; import { showErrorToast } from '../../../../utils/ToastUtils'; +import DisplayName from '../../../common/DisplayName/DisplayName'; import ErrorPlaceHolder from '../../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; import NextPrevious from '../../../common/NextPrevious/NextPrevious'; import { PagingHandlerParams } from '../../../common/NextPrevious/NextPrevious.interface'; +import RichTextEditorPreviewer from '../../../common/RichTextEditor/RichTextEditorPreviewer'; import Searchbar from '../../../common/SearchBarComponent/SearchBar.component'; import Table from '../../../common/Table/Table'; +import { EntityName } from '../../../Modals/EntityNameModal/EntityNameModal.interface'; import { DatabaseSchemaTableProps } from './DatabaseSchemaTable.interface'; export const DatabaseSchemaTable = ({ isDatabaseDeleted, + isVersionPage = false, }: Readonly) => { const { fqn: decodedDatabaseFQN } = useFqn(); const history = useHistory(); const location = useCustomLocation(); + const { permissions } = usePermissionProvider(); + const [schemas, setSchemas] = useState([]); const [isLoading, setIsLoading] = useState(true); const [showDeletedSchemas, setShowDeletedSchemas] = useState(false); + const allowEditDisplayNamePermission = useMemo(() => { + return ( + (!isVersionPage && permissions.databaseSchema.EditAll) || + permissions.databaseSchema.EditDisplayName + ); + }, [permissions, isVersionPage]); + const searchValue = useMemo(() => { const param = location.search; const searchData = QueryString.parse( @@ -160,6 +184,95 @@ export const DatabaseSchemaTable = ({ } }; + const handleDisplayNameUpdate = useCallback( + async (data: EntityName, id?: string) => { + try { + const schemaDetails = schemas.find((schema) => schema.id === id); + if (!schemaDetails) { + throw new Error(t('error.tableNotFound')); + } + const updatedData = { ...schemaDetails, displayName: data.displayName }; + const jsonPatch = compare(schemaDetails, updatedData); + await patchDatabaseSchemaDetails(schemaDetails.id, jsonPatch); + setSchemas((prevData) => + prevData.map((schema) => + schema.id === id + ? { ...schema, displayName: data.displayName } + : schema + ) + ); + } catch (error) { + showErrorToast(error as AxiosError); + } + }, + [schemas, t] + ); + + const schemaTableColumns: ColumnsType = useMemo( + () => [ + { + title: t('label.schema-name'), + dataIndex: 'name', + key: 'name', + width: 250, + render: (_, record: DatabaseSchema) => ( + + ), + }, + { + title: t('label.description'), + dataIndex: 'description', + key: 'description', + render: (text: string) => + text?.trim() ? ( + + ) : ( + + {t('label.no-entity', { entity: t('label.description') })} + + ), + }, + { + title: t('label.owner-plural'), + dataIndex: 'owners', + key: 'owners', + width: 120, + render: (owners: EntityReference[]) => + !isUndefined(owners) && owners.length > 0 ? ( + owners.map((owner: EntityReference) => getEntityName(owner)) + ) : ( + + {NO_DATA_PLACEHOLDER} + + ), + }, + { + title: t('label.usage'), + dataIndex: 'usageSummary', + key: 'usageSummary', + width: 120, + render: (text: UsageDetails) => + getUsagePercentile(text?.weeklyStats?.percentileRank ?? 0), + }, + ], + [handleDisplayNameUpdate, allowEditDisplayNamePermission] + ); + useEffect(() => { fetchDatabaseSchema(); }, [decodedDatabaseFQN, pageSize, showDeletedSchemas, isDatabaseDeleted]); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Modals/EntityNameModal/EntityNameModal.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Modals/EntityNameModal/EntityNameModal.interface.ts index 4f57e3699588..2106081055e3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Modals/EntityNameModal/EntityNameModal.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Modals/EntityNameModal/EntityNameModal.interface.ts @@ -13,7 +13,7 @@ import { Rule } from 'antd/lib/form'; import { Constraint } from '../../../generated/entity/data/table'; -export type EntityName = { name: string; displayName?: string }; +export type EntityName = { name: string; displayName?: string; id?: string }; export type EntityNameWithAdditionFields = EntityName & { constraint: Constraint; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.test.tsx new file mode 100644 index 000000000000..f2633a5c8a1c --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.test.tsx @@ -0,0 +1,106 @@ +/* + * 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 React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import DisplayName, { DisplayNameProps } from './DisplayName'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + Link: jest + .fn() + .mockImplementation(({ children, ...props }) => ( + {children} + )), +})); + +jest.mock('../../../constants/constants', () => ({ + DE_ACTIVE_COLOR: '#BFBFBF', + ICON_DIMENSION: { width: 16, height: 16 }, +})); + +jest.mock('../../Modals/EntityNameModal/EntityNameModal.component', () => { + return { + __esModule: true, + default: jest.fn(() => ( +
Mocked Modal
+ )), + }; +}); + +const mockOnEditDisplayName = jest.fn(); + +const mockProps: DisplayNameProps = { + id: '1', + name: 'Sample Entity', + displayName: 'Sample Display Name', + link: '/entity/1', + allowRename: true, + onEditDisplayName: mockOnEditDisplayName, +}; + +describe('Test DisplayName Component', () => { + it('Should render the component with the display name', async () => { + await act(async () => { + render( + + + + ); + + const displayNameField = await screen.getByTestId('column-display-name'); + + expect(displayNameField).toBeInTheDocument(); + expect(displayNameField).toHaveTextContent('Sample Display Name'); + + const editButton = await screen.queryByTestId('edit-displayName-button'); + + expect(editButton).toBeInTheDocument(); + }); + }); + + it('Should render the component with name when display name is empty', async () => { + await act(async () => { + render( + + + + ); + + const nameField = await screen.getByTestId('column-name'); + + expect(nameField).toBeInTheDocument(); + expect(nameField).toHaveTextContent('Sample Entity'); + }); + }); + + it('Should open the edit modal on edit button click', async () => { + await act(async () => { + render( + + + + ); + const editButton = screen.getByTestId('edit-displayName-button'); + fireEvent.click(editButton); + + const nameField = await screen.findByTestId('column-name'); + + expect(nameField).toBeInTheDocument(); + + const displayNameField = await screen.findByTestId('column-display-name'); + + expect(displayNameField).toBeInTheDocument(); + }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.tsx new file mode 100644 index 000000000000..f4b7f382f35a --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.tsx @@ -0,0 +1,121 @@ +/* + * 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 { Button, Tooltip, Typography } from 'antd'; +import { AxiosError } from 'axios'; +import { isEmpty } from 'lodash'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Link } from 'react-router-dom'; +import { ReactComponent as IconEdit } from '../../../assets/svg/edit-new.svg'; +import { DE_ACTIVE_COLOR, ICON_DIMENSION } from '../../../constants/constants'; +import { showErrorToast } from '../../../utils/ToastUtils'; +import EntityNameModal from '../../Modals/EntityNameModal/EntityNameModal.component'; +import { EntityName } from '../../Modals/EntityNameModal/EntityNameModal.interface'; + +export interface DisplayNameProps { + id: string; + name?: string; + displayName?: string; + link: string; + onEditDisplayName?: (data: EntityName, id?: string) => Promise; + allowRename: boolean; +} + +const DisplayName: React.FC = ({ + id, + name, + displayName, + onEditDisplayName, + link, + allowRename, +}) => { + const { t } = useTranslation(); + + const [isDisplayNameEditing, setIsDisplayNameEditing] = useState(false); + + const handleDisplayNameUpdate = async (data: EntityName) => { + if (!onEditDisplayName) { + return; + } + setIsDisplayNameEditing(true); + try { + await onEditDisplayName(data, id); + } catch (error) { + showErrorToast(error as AxiosError); + } finally { + setIsDisplayNameEditing(false); + } + }; + + return ( +
+
+ + {isEmpty(displayName) ? ( + + {name} + + ) : ( + name + )} + +
+ {!isEmpty(displayName) ? ( + // It will render displayName fallback to name + + + {displayName} + + + ) : null} + + {allowRename ? ( + + + + ) : null} + {isDisplayNameEditing && ( + setIsDisplayNameEditing(false)} + onSave={handleDisplayNameUpdate} + /> + )} +
+ ); +}; + +export default DisplayName; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx index d9ef74254263..4ab17050e9b8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx @@ -13,23 +13,29 @@ import { Col, Row, Switch, Typography } from 'antd'; import { ColumnsType } from 'antd/lib/table'; +import { AxiosError } from 'axios'; +import { compare } from 'fast-json-patch'; import { isEmpty, isUndefined } from 'lodash'; -import React, { useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Link } from 'react-router-dom'; +import DisplayName from '../../components/common/DisplayName/DisplayName'; import DescriptionV1 from '../../components/common/EntityDescription/DescriptionV1'; import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import NextPrevious from '../../components/common/NextPrevious/NextPrevious'; import { NextPreviousProps } from '../../components/common/NextPrevious/NextPrevious.interface'; import RichTextEditorPreviewer from '../../components/common/RichTextEditor/RichTextEditorPreviewer'; import TableAntd from '../../components/common/Table/Table'; +import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; +import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider'; import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; import { EntityType } from '../../enums/entity.enum'; import { DatabaseSchema } from '../../generated/entity/data/databaseSchema'; import { Table } from '../../generated/entity/data/table'; import { UsePagingInterface } from '../../hooks/paging/usePaging'; +import { patchTableDetails } from '../../rest/tableAPI'; import entityUtilClassBase from '../../utils/EntityUtilClassBase'; import { getEntityName } from '../../utils/EntityUtils'; +import { showErrorToast } from '../../utils/ToastUtils'; interface SchemaTablesTabProps { databaseSchemaDetails: DatabaseSchema; @@ -69,6 +75,46 @@ function SchemaTablesTab({ pagingInfo, }: Readonly) { const { t } = useTranslation(); + const [localTableData, setLocalTableData] = useState([]); + + const { permissions } = usePermissionProvider(); + + const allowEditDisplayNamePermission = useMemo(() => { + return ( + !isVersionView && + (permissions.table.EditAll || permissions.table.EditDisplayName) + ); + }, [permissions, isVersionView]); + + const handleDisplayNameUpdate = useCallback( + async (data: EntityName, id?: string) => { + try { + const tableDetails = localTableData.find((table) => table.id === id); + if (!tableDetails) { + throw new Error(t('error.tableNotFound')); + } + + const updatedData = { ...tableDetails, displayName: data.displayName }; + const jsonPatch = compare(tableDetails, updatedData); + await patchTableDetails(tableDetails.id, jsonPatch); + + setLocalTableData((prevData) => + prevData.map((table) => + table.id === id + ? { ...table, displayName: data.displayName } + : table + ) + ); + } catch (error) { + showErrorToast(error as AxiosError); + } + }, + [localTableData, t] + ); + + useEffect(() => { + setLocalTableData(tableData); + }, [tableData]); const tableColumn: ColumnsType = useMemo( () => [ @@ -79,17 +125,18 @@ function SchemaTablesTab({ width: 500, render: (_, record: Table) => { return ( -
- - {getEntityName(record)} - -
+ ); }, }, @@ -105,7 +152,7 @@ function SchemaTablesTab({ ), }, ], - [] + [handleDisplayNameUpdate, allowEditDisplayNamePermission] ); return ( @@ -158,7 +205,7 @@ function SchemaTablesTab({ bordered columns={tableColumn} data-testid="databaseSchema-tables" - dataSource={tableData} + dataSource={localTableData} loading={tableDataLoading} locale={{ emptyText: ( diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseVersionPage/DatabaseVersionPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseVersionPage/DatabaseVersionPage.tsx index dc7eef110c1f..84eeee904ffe 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseVersionPage/DatabaseVersionPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseVersionPage/DatabaseVersionPage.tsx @@ -209,7 +209,7 @@ function DatabaseVersionPage() { /> - + diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceMainTabContent.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceMainTabContent.tsx index 5a7f570e0711..1bb780ce69ab 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceMainTabContent.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceMainTabContent.tsx @@ -13,9 +13,11 @@ import { Col, Row, Space, Switch, Table, Typography } from 'antd'; import { ColumnsType } from 'antd/lib/table'; +import { AxiosError } from 'axios'; +import { compare } from 'fast-json-patch'; import { isUndefined } from 'lodash'; import { EntityTags, ServiceTypes } from 'Models'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import DescriptionV1 from '../../components/common/EntityDescription/DescriptionV1'; @@ -25,7 +27,9 @@ import NextPrevious from '../../components/common/NextPrevious/NextPrevious'; import { NextPreviousProps } from '../../components/common/NextPrevious/NextPrevious.interface'; import ResizablePanels from '../../components/common/ResizablePanels/ResizablePanels'; import EntityRightPanel from '../../components/Entity/EntityRightPanel/EntityRightPanel'; +import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import { COMMON_RESIZABLE_PANEL_CONFIG } from '../../constants/ResizablePanel.constants'; +import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider'; import { OperationPermission } from '../../context/PermissionProvider/PermissionProvider.interface'; import { EntityType } from '../../enums/entity.enum'; import { DatabaseService } from '../../generated/entity/services/databaseService'; @@ -33,10 +37,19 @@ import { Paging } from '../../generated/type/paging'; import { UsePagingInterface } from '../../hooks/paging/usePaging'; import { useFqn } from '../../hooks/useFqn'; import { ServicesType } from '../../interface/service.interface'; +import { patchApiCollection } from '../../rest/apiCollectionsAPI'; +import { patchDashboardDetails } from '../../rest/dashboardAPI'; +import { patchDatabaseDetails } from '../../rest/databaseAPI'; +import { patchMlModelDetails } from '../../rest/mlModelAPI'; +import { patchPipelineDetails } from '../../rest/pipelineAPI'; +import { patchSearchIndexDetails } from '../../rest/SearchIndexAPI'; +import { patchContainerDetails } from '../../rest/storageAPI'; +import { patchTopicDetails } from '../../rest/topicsAPI'; import { getServiceMainTabColumns } from '../../utils/ServiceMainTabContentUtils'; import { getEntityTypeFromServiceCategory } from '../../utils/ServiceUtils'; import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils'; import { createTagObject } from '../../utils/TagsUtils'; +import { showErrorToast } from '../../utils/ToastUtils'; import { ServicePageData } from './ServiceDetailsPage'; interface ServiceMainTabContentProps { @@ -53,6 +66,7 @@ interface ServiceMainTabContentProps { pagingHandler: NextPreviousProps['pagingHandler']; saveUpdatedServiceData: (updatedData: ServicesType) => Promise; pagingInfo: UsePagingInterface; + isVersionPage?: boolean; } function ServiceMainTabContent({ @@ -69,6 +83,7 @@ function ServiceMainTabContent({ serviceDetails, saveUpdatedServiceData, pagingInfo, + isVersionPage = false, }: Readonly) { const { t } = useTranslation(); const { serviceCategory } = useParams<{ @@ -76,7 +91,10 @@ function ServiceMainTabContent({ }>(); const { fqn: serviceFQN } = useFqn(); + const { permissions } = usePermissionProvider(); + const [isEdit, setIsEdit] = useState(false); + const [pageData, setPageData] = useState([]); const tier = getTierTags(serviceDetails?.tags ?? []); const tags = getTagsWithoutTier(serviceDetails?.tags ?? []); @@ -131,9 +149,96 @@ function ServiceMainTabContent({ setIsEdit(false); }; + const callServicePatchAPI = async ( + serviceCategory: ServiceTypes, + id: string, + jsonPatch: any + ) => { + switch (serviceCategory) { + case 'databaseServices': + return await patchDatabaseDetails(id, jsonPatch); + case 'messagingServices': + return await patchTopicDetails(id, jsonPatch); + case 'dashboardServices': + return await patchDashboardDetails(id, jsonPatch); + case 'pipelineServices': + return await patchPipelineDetails(id, jsonPatch); + case 'mlmodelServices': + return await patchMlModelDetails(id, jsonPatch); + case 'storageServices': + return await patchContainerDetails(id, jsonPatch); + case 'searchServices': + return await patchSearchIndexDetails(id, jsonPatch); + case 'apiServices': + return await patchApiCollection(id, jsonPatch); + default: + throw new Error('Unsupported service category'); + } + }; + + const handleDisplayNameUpdate = useCallback( + async (entityData: EntityName, id?: string) => { + try { + const pageDataDetails = pageData.find((data) => data.id === id); + if (!pageDataDetails) { + throw new Error(t('error.databaseNotFound')); + } + const updatedData = { + ...pageDataDetails, + displayName: entityData.displayName, + }; + const jsonPatch = compare(pageDataDetails, updatedData); + await callServicePatchAPI( + serviceCategory, + pageDataDetails.id, + jsonPatch + ); + setPageData((prevData) => + prevData.map((data) => + data.id === id + ? { ...data, displayName: entityData.displayName } + : data + ) + ); + } catch (error) { + showErrorToast(error as AxiosError); + } + }, + [pageData, serviceCategory] + ); + + const editDisplayNamePermission = useMemo(() => { + if (isVersionPage) { + return false; + } + + const servicePermissions = { + databaseServices: permissions.databaseService, + messagingServices: permissions.messagingService, + dashboardServices: permissions.dashboardService, + pipelineServices: permissions.pipelineService, + mlmodelServices: permissions.mlmodelService, + storageServices: permissions.storageService, + searchServices: permissions.searchService, + apiServices: permissions.apiService, + }; + + const currentPermission = + servicePermissions[serviceCategory as keyof typeof servicePermissions]; + + return ( + currentPermission?.EditAll || currentPermission?.EditDisplayName || false + ); + }, [permissions, serviceCategory, isVersionPage]); + const tableColumn: ColumnsType = useMemo( - () => getServiceMainTabColumns(serviceCategory), - [serviceCategory] + () => + getServiceMainTabColumns( + serviceCategory, + editDisplayNamePermission, + handleDisplayNameUpdate + ), + [serviceCategory, handleDisplayNameUpdate, editDisplayNamePermission] ); const entityType = useMemo( @@ -153,6 +258,10 @@ function ServiceMainTabContent({ [servicePermission, serviceDetails] ); + useEffect(() => { + setPageData(data); + }, [data]); + return ( @@ -203,7 +312,7 @@ function ServiceMainTabContent({ bordered columns={tableColumn} data-testid="service-children-table" - dataSource={data} + dataSource={pageData} locale={{ emptyText: , }} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionMainTabContent.interface.ts b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionMainTabContent.interface.ts index cf0100aabb1b..100aa7c306d3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionMainTabContent.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionMainTabContent.interface.ts @@ -28,4 +28,5 @@ export interface ServiceVersionMainTabContentProps { pagingHandler: NextPreviousProps['pagingHandler']; entityType: EntityType; changeDescription: ChangeDescription; + isVersionPage?: boolean; } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionMainTabContent.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionMainTabContent.tsx index ff362630d74a..c96c53c06747 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionMainTabContent.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionMainTabContent.tsx @@ -25,6 +25,7 @@ import TagsContainerV2 from '../../components/Tag/TagsContainerV2/TagsContainerV import { DisplayType } from '../../components/Tag/TagsViewer/TagsViewer.interface'; import { PAGE_SIZE } from '../../constants/constants'; import { TABLE_SCROLL_VALUE } from '../../constants/Table.constants'; +import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider'; import { TagSource } from '../../generated/type/tagLabel'; import { useFqn } from '../../hooks/useFqn'; import { getCommonDiffsFromVersionData } from '../../utils/EntityVersionUtils'; @@ -42,15 +43,24 @@ function ServiceVersionMainTabContent({ serviceDetails, entityType, changeDescription, + isVersionPage = true, }: ServiceVersionMainTabContentProps) { const { serviceCategory } = useParams<{ serviceCategory: ServiceTypes; }>(); const { fqn: serviceFQN } = useFqn(); + const { permissions } = usePermissionProvider(); + + const editDisplayNamePermission = useMemo(() => { + return !isVersionPage + ? permissions.databaseService.EditAll || + permissions.databaseService.EditDisplayName + : false; + }, [permissions]); const tableColumn: ColumnsType = useMemo( - () => getServiceMainTabColumns(serviceCategory), + () => getServiceMainTabColumns(serviceCategory, editDisplayNamePermission), [serviceCategory] ); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceMainTabContentUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceMainTabContentUtils.tsx index 2284f499a95d..17bd33518eac 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceMainTabContentUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceMainTabContentUtils.tsx @@ -17,9 +17,10 @@ import { t } from 'i18next'; import { isUndefined } from 'lodash'; import { ServiceTypes } from 'Models'; import React from 'react'; -import { Link } from 'react-router-dom'; +import DisplayName from '../components/common/DisplayName/DisplayName'; import UserPopOverCard from '../components/common/PopOverCard/UserPopOverCard'; import RichTextEditorPreviewer from '../components/common/RichTextEditor/RichTextEditorPreviewer'; +import { EntityName } from '../components/Modals/EntityNameModal/EntityNameModal.interface'; import TagsViewer from '../components/Tag/TagsViewer/TagsViewer'; import { NO_DATA_PLACEHOLDER } from '../constants/constants'; import { ServiceCategory } from '../enums/service.enum'; @@ -28,35 +29,33 @@ import { Database } from '../generated/entity/data/database'; import { Pipeline } from '../generated/entity/data/pipeline'; import { EntityReference } from '../generated/entity/type'; import { ServicePageData } from '../pages/ServiceDetailsPage/ServiceDetailsPage'; -import { getEntityName } from './EntityUtils'; import { getLinkForFqn } from './ServiceUtils'; import { getUsagePercentile } from './TableUtils'; export const getServiceMainTabColumns = ( - serviceCategory: ServiceTypes + serviceCategory: ServiceTypes, + editDisplayNamePermission: boolean, + handleDisplayNameUpdate?: ( + entityData: EntityName, + id?: string + ) => Promise ): ColumnsType => [ { title: t('label.name'), dataIndex: 'name', key: 'name', width: 280, - render: (_, record: ServicePageData) => { - return ( - - - {getEntityName(record)} - - - ); - }, + render: (_, record: ServicePageData) => ( + + ), }, { title: t('label.description'), From c98128f8e432f1b81bbf5be04f4bb0168f4cfa61 Mon Sep 17 00:00:00 2001 From: Pranita Date: Fri, 22 Nov 2024 11:50:14 +0530 Subject: [PATCH 2/8] refactor: logic and style changes --- .../DatabaseSchemaTable.tsx | 17 +++--- .../common/DisplayName/DisplayName.test.tsx | 15 ++--- .../common/DisplayName/DisplayName.tsx | 57 ++++++++----------- .../DatabaseSchemaPage/SchemaTablesTab.tsx | 4 +- .../ServiceMainTabContent.tsx | 42 ++------------ .../src/main/resources/ui/src/styles/app.less | 6 ++ .../src/utils/ServiceMainTabContentUtils.tsx | 37 +++++++++++- .../main/resources/ui/webpack.config.dev.js | 3 +- 8 files changed, 89 insertions(+), 92 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable.tsx index 26b02d32a2ef..d19c12c1a899 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable.tsx @@ -15,7 +15,7 @@ import { ColumnsType } from 'antd/lib/table'; import { AxiosError } from 'axios'; import { compare } from 'fast-json-patch'; import { t } from 'i18next'; -import { isEmpty, isUndefined } from 'lodash'; +import { isEmpty } from 'lodash'; import QueryString from 'qs'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useHistory } from 'react-router-dom'; @@ -69,8 +69,9 @@ export const DatabaseSchemaTable = ({ const allowEditDisplayNamePermission = useMemo(() => { return ( - (!isVersionPage && permissions.databaseSchema.EditAll) || - permissions.databaseSchema.EditDisplayName + !isVersionPage && + (permissions.databaseSchema.EditAll || + permissions.databaseSchema.EditDisplayName) ); }, [permissions, isVersionPage]); @@ -189,11 +190,11 @@ export const DatabaseSchemaTable = ({ try { const schemaDetails = schemas.find((schema) => schema.id === id); if (!schemaDetails) { - throw new Error(t('error.tableNotFound')); + return; } const updatedData = { ...schemaDetails, displayName: data.displayName }; const jsonPatch = compare(schemaDetails, updatedData); - await patchDatabaseSchemaDetails(schemaDetails.id, jsonPatch); + await patchDatabaseSchemaDetails(schemaDetails.id ?? '', jsonPatch); setSchemas((prevData) => prevData.map((schema) => schema.id === id @@ -205,7 +206,7 @@ export const DatabaseSchemaTable = ({ showErrorToast(error as AxiosError); } }, - [schemas, t] + [schemas] ); const schemaTableColumns: ColumnsType = useMemo( @@ -219,7 +220,7 @@ export const DatabaseSchemaTable = ({ - !isUndefined(owners) && owners.length > 0 ? ( + !isEmpty(owners) && owners.length > 0 ? ( owners.map((owner: EntityReference) => getEntityName(owner)) ) : ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.test.tsx index f2633a5c8a1c..79ef6f86f459 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.test.tsx @@ -29,14 +29,9 @@ jest.mock('../../../constants/constants', () => ({ ICON_DIMENSION: { width: 16, height: 16 }, })); -jest.mock('../../Modals/EntityNameModal/EntityNameModal.component', () => { - return { - __esModule: true, - default: jest.fn(() => ( -
Mocked Modal
- )), - }; -}); +jest.mock('../../Modals/EntityNameModal/EntityNameModal.component', () => + jest.fn().mockImplementation(() =>

Mocked Modal

) +); const mockOnEditDisplayName = jest.fn(); @@ -63,7 +58,7 @@ describe('Test DisplayName Component', () => { expect(displayNameField).toBeInTheDocument(); expect(displayNameField).toHaveTextContent('Sample Display Name'); - const editButton = await screen.queryByTestId('edit-displayName-button'); + const editButton = screen.queryByTestId('edit-displayName-button'); expect(editButton).toBeInTheDocument(); }); @@ -77,7 +72,7 @@ describe('Test DisplayName Component', () => { ); - const nameField = await screen.getByTestId('column-name'); + const nameField = screen.getByTestId('column-name'); expect(nameField).toBeInTheDocument(); expect(nameField).toHaveTextContent('Sample Entity'); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.tsx index f4b7f382f35a..b4dd3044cbc1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.tsx @@ -44,12 +44,9 @@ const DisplayName: React.FC = ({ const [isDisplayNameEditing, setIsDisplayNameEditing] = useState(false); const handleDisplayNameUpdate = async (data: EntityName) => { - if (!onEditDisplayName) { - return; - } setIsDisplayNameEditing(true); try { - await onEditDisplayName(data, id); + await onEditDisplayName?.(data, id); } catch (error) { showErrorToast(error as AxiosError); } finally { @@ -59,44 +56,40 @@ const DisplayName: React.FC = ({ return (
-
- - {isEmpty(displayName) ? ( - - {name} - - ) : ( - name - )} - -
- {!isEmpty(displayName) ? ( - // It will render displayName fallback to name - + + {isEmpty(displayName) ? ( - {displayName} + {name} - - ) : null} + ) : ( + <> + {name} + + + {displayName} + + + + )} + {allowRename ? ( + onClick={() => setIsDisplayNameEditing(true)} + /> ) : null} {isDisplayNameEditing && ( diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx index 4ab17050e9b8..282231f700e0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx @@ -91,7 +91,7 @@ function SchemaTablesTab({ try { const tableDetails = localTableData.find((table) => table.id === id); if (!tableDetails) { - throw new Error(t('error.tableNotFound')); + return; } const updatedData = { ...tableDetails, displayName: data.displayName }; @@ -109,7 +109,7 @@ function SchemaTablesTab({ showErrorToast(error as AxiosError); } }, - [localTableData, t] + [localTableData] ); useEffect(() => { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceMainTabContent.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceMainTabContent.tsx index 0c0f21590870..4ed02713332f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceMainTabContent.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceMainTabContent.tsx @@ -37,15 +37,10 @@ import { Paging } from '../../generated/type/paging'; import { UsePagingInterface } from '../../hooks/paging/usePaging'; import { useFqn } from '../../hooks/useFqn'; import { ServicesType } from '../../interface/service.interface'; -import { patchApiCollection } from '../../rest/apiCollectionsAPI'; -import { patchDashboardDetails } from '../../rest/dashboardAPI'; -import { patchDatabaseDetails } from '../../rest/databaseAPI'; -import { patchMlModelDetails } from '../../rest/mlModelAPI'; -import { patchPipelineDetails } from '../../rest/pipelineAPI'; -import { patchSearchIndexDetails } from '../../rest/SearchIndexAPI'; -import { patchContainerDetails } from '../../rest/storageAPI'; -import { patchTopicDetails } from '../../rest/topicsAPI'; -import { getServiceMainTabColumns } from '../../utils/ServiceMainTabContentUtils'; +import { + callServicePatchAPI, + getServiceMainTabColumns, +} from '../../utils/ServiceMainTabContentUtils'; import { getEntityTypeFromServiceCategory } from '../../utils/ServiceUtils'; import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils'; import { createTagObject } from '../../utils/TagsUtils'; @@ -149,39 +144,12 @@ function ServiceMainTabContent({ setIsEdit(false); }; - const callServicePatchAPI = async ( - serviceCategory: ServiceTypes, - id: string, - jsonPatch: any - ) => { - switch (serviceCategory) { - case 'databaseServices': - return await patchDatabaseDetails(id, jsonPatch); - case 'messagingServices': - return await patchTopicDetails(id, jsonPatch); - case 'dashboardServices': - return await patchDashboardDetails(id, jsonPatch); - case 'pipelineServices': - return await patchPipelineDetails(id, jsonPatch); - case 'mlmodelServices': - return await patchMlModelDetails(id, jsonPatch); - case 'storageServices': - return await patchContainerDetails(id, jsonPatch); - case 'searchServices': - return await patchSearchIndexDetails(id, jsonPatch); - case 'apiServices': - return await patchApiCollection(id, jsonPatch); - default: - throw new Error('Unsupported service category'); - } - }; - const handleDisplayNameUpdate = useCallback( async (entityData: EntityName, id?: string) => { try { const pageDataDetails = pageData.find((data) => data.id === id); if (!pageDataDetails) { - throw new Error(t('error.databaseNotFound')); + return; } const updatedData = { ...pageDataDetails, 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 8fc1c0141785..3cdb74ad710d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/app.less +++ b/openmetadata-ui/src/main/resources/ui/src/styles/app.less @@ -838,3 +838,9 @@ a[href].link-text-grey, margin-right: 64px; } } + +.display-name-edit-button { + padding: 0; + border: none; + background-color: transparent; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceMainTabContentUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceMainTabContentUtils.tsx index 68b457896e86..405ca57f627a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceMainTabContentUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceMainTabContentUtils.tsx @@ -18,8 +18,6 @@ import { isUndefined } from 'lodash'; import { ServiceTypes } from 'Models'; import React from 'react'; import DisplayName from '../components/common/DisplayName/DisplayName'; -import UserPopOverCard from '../components/common/PopOverCard/UserPopOverCard'; -import { Link } from 'react-router-dom'; import { OwnerLabel } from '../components/common/OwnerLabel/OwnerLabel.component'; import RichTextEditorPreviewer from '../components/common/RichTextEditor/RichTextEditorPreviewer'; import { EntityName } from '../components/Modals/EntityNameModal/EntityNameModal.interface'; @@ -29,6 +27,14 @@ import { ServiceCategory } from '../enums/service.enum'; import { Database } from '../generated/entity/data/database'; import { Pipeline } from '../generated/entity/data/pipeline'; import { ServicePageData } from '../pages/ServiceDetailsPage/ServiceDetailsPage'; +import { patchApiCollection } from '../rest/apiCollectionsAPI'; +import { patchDashboardDetails } from '../rest/dashboardAPI'; +import { patchDatabaseDetails } from '../rest/databaseAPI'; +import { patchMlModelDetails } from '../rest/mlModelAPI'; +import { patchPipelineDetails } from '../rest/pipelineAPI'; +import { patchSearchIndexDetails } from '../rest/SearchIndexAPI'; +import { patchContainerDetails } from '../rest/storageAPI'; +import { patchTopicDetails } from '../rest/topicsAPI'; import { getLinkForFqn } from './ServiceUtils'; import { getUsagePercentile } from './TableUtils'; @@ -124,3 +130,30 @@ export const getServiceMainTabColumns = ( ] : []), ]; + +export const callServicePatchAPI = async ( + serviceCategory: ServiceTypes, + id: string, + jsonPatch: any +) => { + switch (serviceCategory) { + case ServiceCategory.DATABASE_SERVICES: + return await patchDatabaseDetails(id, jsonPatch); + case ServiceCategory.MESSAGING_SERVICES: + return await patchTopicDetails(id, jsonPatch); + case ServiceCategory.DASHBOARD_SERVICES: + return await patchDashboardDetails(id, jsonPatch); + case ServiceCategory.PIPELINE_SERVICES: + return await patchPipelineDetails(id, jsonPatch); + case ServiceCategory.ML_MODEL_SERVICES: + return await patchMlModelDetails(id, jsonPatch); + case ServiceCategory.STORAGE_SERVICES: + return await patchContainerDetails(id, jsonPatch); + case ServiceCategory.SEARCH_SERVICES: + return await patchSearchIndexDetails(id, jsonPatch); + case ServiceCategory.API_SERVICES: + return await patchApiCollection(id, jsonPatch); + default: + throw new Error('Unsupported service category'); + } +}; diff --git a/openmetadata-ui/src/main/resources/ui/webpack.config.dev.js b/openmetadata-ui/src/main/resources/ui/webpack.config.dev.js index 178b65174629..2f7c3c00a57f 100644 --- a/openmetadata-ui/src/main/resources/ui/webpack.config.dev.js +++ b/openmetadata-ui/src/main/resources/ui/webpack.config.dev.js @@ -19,7 +19,8 @@ const process = require('process'); const outputPath = path.join(__dirname, 'build'); const subPath = process.env.APP_SUB_PATH ?? ''; -const devServerTarget = process.env.DEV_SERVER_TARGET ?? 'http://localhost:8585/'; +const devServerTarget = + process.env.DEV_SERVER_TARGET ?? 'https://sandbox-beta.open-metadata.org/'; module.exports = { // Development mode From 17c3b669612b112d057a3284874a5d400c250d95 Mon Sep 17 00:00:00 2001 From: Pranita Date: Fri, 22 Nov 2024 13:03:38 +0530 Subject: [PATCH 3/8] fix: minor changes --- .../ui/src/components/common/DisplayName/DisplayName.tsx | 2 +- openmetadata-ui/src/main/resources/ui/src/styles/app.less | 7 +++++++ .../resources/ui/src/utils/ServiceMainTabContentUtils.tsx | 2 +- .../src/main/resources/ui/webpack.config.dev.js | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.tsx index b4dd3044cbc1..2ead6db3383d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.tsx @@ -80,7 +80,7 @@ const DisplayName: React.FC = ({ {allowRename ? (