Feat: edit display name (#18720)

* feat: edit display name

* refactor: logic and style changes

* fix: minor changes

* style: update style for edit button

* style: remove inline styles and update hover effect

* refactor: remove edit permission for version page and fix handleEditDisplayName function

* refactor: updated displayName

* fix: data-testid for the Link

---------

Co-authored-by: Shailesh Parmar <shailesh.parmar.webdev@gmail.com>
This commit is contained in:
Pranita Fulsundar 2024-11-28 19:49:58 +05:30 committed by GitHub
parent cb33f274fc
commit 9d37ff0e34
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 550 additions and 45 deletions

View File

@ -13,4 +13,5 @@
export interface DatabaseSchemaTableProps { export interface DatabaseSchemaTableProps {
isDatabaseDeleted?: boolean; isDatabaseDeleted?: boolean;
isVersionPage?: boolean;
} }

View File

@ -11,45 +11,70 @@
* limitations under the License. * limitations under the License.
*/ */
import { Col, Row, Switch, Typography } from 'antd'; import { Col, Row, Switch, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { compare } from 'fast-json-patch';
import { t } from 'i18next'; import { t } from 'i18next';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import QueryString from 'qs'; import QueryString from 'qs';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { import {
getEntityDetailsPath,
INITIAL_PAGING_VALUE, INITIAL_PAGING_VALUE,
NO_DATA_PLACEHOLDER,
PAGE_SIZE, PAGE_SIZE,
} from '../../../../constants/constants'; } 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 { SearchIndex } from '../../../../enums/search.enum';
import { DatabaseSchema } from '../../../../generated/entity/data/databaseSchema'; 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 { Include } from '../../../../generated/type/include';
import { Paging } from '../../../../generated/type/paging'; import { Paging } from '../../../../generated/type/paging';
import { usePaging } from '../../../../hooks/paging/usePaging'; import { usePaging } from '../../../../hooks/paging/usePaging';
import useCustomLocation from '../../../../hooks/useCustomLocation/useCustomLocation'; import useCustomLocation from '../../../../hooks/useCustomLocation/useCustomLocation';
import { useFqn } from '../../../../hooks/useFqn'; import { useFqn } from '../../../../hooks/useFqn';
import { getDatabaseSchemas } from '../../../../rest/databaseAPI'; import {
getDatabaseSchemas,
patchDatabaseSchemaDetails,
} from '../../../../rest/databaseAPI';
import { searchQuery } from '../../../../rest/searchAPI'; 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 { showErrorToast } from '../../../../utils/ToastUtils';
import DisplayName from '../../../common/DisplayName/DisplayName';
import ErrorPlaceHolder from '../../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; import ErrorPlaceHolder from '../../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
import NextPrevious from '../../../common/NextPrevious/NextPrevious'; import NextPrevious from '../../../common/NextPrevious/NextPrevious';
import { PagingHandlerParams } from '../../../common/NextPrevious/NextPrevious.interface'; import { PagingHandlerParams } from '../../../common/NextPrevious/NextPrevious.interface';
import RichTextEditorPreviewer from '../../../common/RichTextEditor/RichTextEditorPreviewer';
import Searchbar from '../../../common/SearchBarComponent/SearchBar.component'; import Searchbar from '../../../common/SearchBarComponent/SearchBar.component';
import Table from '../../../common/Table/Table'; import Table from '../../../common/Table/Table';
import { EntityName } from '../../../Modals/EntityNameModal/EntityNameModal.interface';
import { DatabaseSchemaTableProps } from './DatabaseSchemaTable.interface'; import { DatabaseSchemaTableProps } from './DatabaseSchemaTable.interface';
export const DatabaseSchemaTable = ({ export const DatabaseSchemaTable = ({
isDatabaseDeleted, isDatabaseDeleted,
isVersionPage = false,
}: Readonly<DatabaseSchemaTableProps>) => { }: Readonly<DatabaseSchemaTableProps>) => {
const { fqn: decodedDatabaseFQN } = useFqn(); const { fqn: decodedDatabaseFQN } = useFqn();
const history = useHistory(); const history = useHistory();
const location = useCustomLocation(); const location = useCustomLocation();
const { permissions } = usePermissionProvider();
const [schemas, setSchemas] = useState<DatabaseSchema[]>([]); const [schemas, setSchemas] = useState<DatabaseSchema[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [showDeletedSchemas, setShowDeletedSchemas] = useState<boolean>(false); const [showDeletedSchemas, setShowDeletedSchemas] = useState<boolean>(false);
const allowEditDisplayNamePermission = useMemo(() => {
return (
!isVersionPage &&
(permissions.databaseSchema.EditAll ||
permissions.databaseSchema.EditDisplayName)
);
}, [permissions, isVersionPage]);
const searchValue = useMemo(() => { const searchValue = useMemo(() => {
const param = location.search; const param = location.search;
const searchData = QueryString.parse( const searchData = QueryString.parse(
@ -160,6 +185,98 @@ export const DatabaseSchemaTable = ({
} }
}; };
const handleDisplayNameUpdate = useCallback(
async (data: EntityName, id?: string) => {
try {
const schemaDetails = schemas.find((schema) => schema.id === id);
if (!schemaDetails) {
return;
}
const updatedData = {
...schemaDetails,
displayName: data.displayName || undefined,
};
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]
);
const schemaTableColumns: ColumnsType<DatabaseSchema> = useMemo(
() => [
{
title: t('label.schema-name'),
dataIndex: 'name',
key: 'name',
width: 250,
render: (_, record: DatabaseSchema) => (
<DisplayName
allowRename={allowEditDisplayNamePermission}
displayName={record.displayName}
id={record.id ?? ''}
key={record.id}
link={
record.fullyQualifiedName
? getEntityDetailsPath(
EntityType.DATABASE_SCHEMA,
record.fullyQualifiedName
)
: ''
}
name={record.name}
onEditDisplayName={handleDisplayNameUpdate}
/>
),
},
{
title: t('label.description'),
dataIndex: 'description',
key: 'description',
render: (text: string) =>
text?.trim() ? (
<RichTextEditorPreviewer markdown={text} />
) : (
<span className="text-grey-muted">
{t('label.no-entity', { entity: t('label.description') })}
</span>
),
},
{
title: t('label.owner-plural'),
dataIndex: 'owners',
key: 'owners',
width: 120,
render: (owners: EntityReference[]) =>
!isEmpty(owners) && owners.length > 0 ? (
owners.map((owner: EntityReference) => getEntityName(owner))
) : (
<Typography.Text data-testid="no-owner-text">
{NO_DATA_PLACEHOLDER}
</Typography.Text>
),
},
{
title: t('label.usage'),
dataIndex: 'usageSummary',
key: 'usageSummary',
width: 120,
render: (text: UsageDetails) =>
getUsagePercentile(text?.weeklyStats?.percentileRank ?? 0),
},
],
[handleDisplayNameUpdate, allowEditDisplayNamePermission]
);
useEffect(() => { useEffect(() => {
fetchDatabaseSchema(); fetchDatabaseSchema();
}, [decodedDatabaseFQN, pageSize, showDeletedSchemas, isDatabaseDeleted]); }, [decodedDatabaseFQN, pageSize, showDeletedSchemas, isDatabaseDeleted]);

View File

@ -13,7 +13,7 @@
import { Rule } from 'antd/lib/form'; import { Rule } from 'antd/lib/form';
import { Constraint } from '../../../generated/entity/data/table'; 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 & { export type EntityNameWithAdditionFields = EntityName & {
constraint: Constraint; constraint: Constraint;

View File

@ -0,0 +1,22 @@
/*
* 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 { EntityName } from '../../Modals/EntityNameModal/EntityNameModal.interface';
export interface DisplayNameProps {
id: string;
name?: string;
displayName?: string;
link: string;
onEditDisplayName?: (data: EntityName, id?: string) => Promise<void>;
allowRename?: boolean;
}

View File

@ -0,0 +1,102 @@
/*
* 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 from './DisplayName';
import { DisplayNameProps } from './DisplayName.interface';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
Link: jest
.fn()
.mockImplementation(({ children, ...props }) => (
<span {...props}>{children}</span>
)),
}));
jest.mock('../../../constants/constants', () => ({
DE_ACTIVE_COLOR: '#BFBFBF',
ICON_DIMENSION: { width: 16, height: 16 },
}));
jest.mock('../../Modals/EntityNameModal/EntityNameModal.component', () =>
jest.fn().mockImplementation(() => <p>Mocked Modal</p>)
);
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(
<MemoryRouter>
<DisplayName {...mockProps} />
</MemoryRouter>
);
const displayNameField = await screen.getByTestId('column-display-name');
expect(displayNameField).toBeInTheDocument();
expect(displayNameField).toHaveTextContent('Sample Display Name');
const editButton = 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(
<MemoryRouter>
<DisplayName {...mockProps} displayName={undefined} />
</MemoryRouter>
);
const nameField = 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(
<MemoryRouter>
<DisplayName {...mockProps} />
</MemoryRouter>
);
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();
});
});
});

View File

@ -0,0 +1,103 @@
/*
* 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';
import { DisplayNameProps } from './DisplayName.interface';
const DisplayName: React.FC<DisplayNameProps> = ({
id,
name,
displayName,
onEditDisplayName,
link,
allowRename,
}) => {
const { t } = useTranslation();
const [isDisplayNameEditing, setIsDisplayNameEditing] = useState(false);
const handleDisplayNameUpdate = async (data: EntityName) => {
setIsDisplayNameEditing(true);
try {
await onEditDisplayName?.(data, id);
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsDisplayNameEditing(false);
}
};
return (
<div className="flex-column hover-icon-group w-max-full">
<Typography.Text
className="m-b-0 d-block text-grey-muted break-word"
data-testid="column-name">
{isEmpty(displayName) ? (
<Link className="break-word" data-testid={name} to={link}>
{name}
</Link>
) : (
<>
{name}
<Typography.Text
className="m-b-0 d-block break-word"
data-testid="column-display-name">
<Link className="break-word" data-testid={name} to={link}>
{displayName}
</Link>
</Typography.Text>
</>
)}
</Typography.Text>
{allowRename ? (
<Tooltip placement="right" title={t('label.edit')}>
<Button
ghost
className="hover-cell-icon"
data-testid="edit-displayName-button"
icon={<IconEdit color={DE_ACTIVE_COLOR} {...ICON_DIMENSION} />}
type="text"
onClick={() => setIsDisplayNameEditing(true)}
/>
</Tooltip>
) : null}
{isDisplayNameEditing && (
<EntityNameModal
allowRename={allowRename}
entity={{
name: name ?? '',
displayName,
}}
title={t('label.edit-entity', {
entity: t('label.display-name'),
})}
visible={isDisplayNameEditing}
onCancel={() => setIsDisplayNameEditing(false)}
onSave={handleDisplayNameUpdate}
/>
)}
</div>
);
};
export default DisplayName;

View File

@ -13,23 +13,29 @@
import { Col, Row, Switch, Typography } from 'antd'; import { Col, Row, Switch, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table'; import { ColumnsType } from 'antd/lib/table';
import { AxiosError } from 'axios';
import { compare } from 'fast-json-patch';
import { isEmpty, isUndefined } from 'lodash'; import { isEmpty, isUndefined } from 'lodash';
import React, { useMemo } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; 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 DescriptionV1 from '../../components/common/EntityDescription/DescriptionV1';
import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder';
import NextPrevious from '../../components/common/NextPrevious/NextPrevious'; import NextPrevious from '../../components/common/NextPrevious/NextPrevious';
import { NextPreviousProps } from '../../components/common/NextPrevious/NextPrevious.interface'; import { NextPreviousProps } from '../../components/common/NextPrevious/NextPrevious.interface';
import RichTextEditorPreviewer from '../../components/common/RichTextEditor/RichTextEditorPreviewer'; import RichTextEditorPreviewer from '../../components/common/RichTextEditor/RichTextEditorPreviewer';
import TableAntd from '../../components/common/Table/Table'; 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 { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum';
import { EntityType } from '../../enums/entity.enum'; import { EntityType } from '../../enums/entity.enum';
import { DatabaseSchema } from '../../generated/entity/data/databaseSchema'; import { DatabaseSchema } from '../../generated/entity/data/databaseSchema';
import { Table } from '../../generated/entity/data/table'; import { Table } from '../../generated/entity/data/table';
import { UsePagingInterface } from '../../hooks/paging/usePaging'; import { UsePagingInterface } from '../../hooks/paging/usePaging';
import { patchTableDetails } from '../../rest/tableAPI';
import entityUtilClassBase from '../../utils/EntityUtilClassBase'; import entityUtilClassBase from '../../utils/EntityUtilClassBase';
import { getEntityName } from '../../utils/EntityUtils'; import { getEntityName } from '../../utils/EntityUtils';
import { showErrorToast } from '../../utils/ToastUtils';
interface SchemaTablesTabProps { interface SchemaTablesTabProps {
databaseSchemaDetails: DatabaseSchema; databaseSchemaDetails: DatabaseSchema;
@ -69,6 +75,48 @@ function SchemaTablesTab({
pagingInfo, pagingInfo,
}: Readonly<SchemaTablesTabProps>) { }: Readonly<SchemaTablesTabProps>) {
const { t } = useTranslation(); const { t } = useTranslation();
const [localTableData, setLocalTableData] = useState<Table[]>([]);
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) {
return;
}
const updatedData = {
...tableDetails,
displayName: data.displayName || undefined,
};
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]
);
useEffect(() => {
setLocalTableData(tableData);
}, [tableData]);
const tableColumn: ColumnsType<Table> = useMemo( const tableColumn: ColumnsType<Table> = useMemo(
() => [ () => [
@ -79,17 +127,18 @@ function SchemaTablesTab({
width: 500, width: 500,
render: (_, record: Table) => { render: (_, record: Table) => {
return ( return (
<div className="d-inline-flex w-max-90"> <DisplayName
<Link allowRename={allowEditDisplayNamePermission}
className="break-word" displayName={record.displayName}
data-testid={record.name} id={record.id}
to={entityUtilClassBase.getEntityLink( key={record.id}
EntityType.TABLE, link={entityUtilClassBase.getEntityLink(
record.fullyQualifiedName as string EntityType.TABLE,
)}> record.fullyQualifiedName as string
{getEntityName(record)} )}
</Link> name={record.name}
</div> onEditDisplayName={handleDisplayNameUpdate}
/>
); );
}, },
}, },
@ -105,7 +154,7 @@ function SchemaTablesTab({
), ),
}, },
], ],
[] [handleDisplayNameUpdate, allowEditDisplayNamePermission]
); );
return ( return (
@ -158,7 +207,7 @@ function SchemaTablesTab({
bordered bordered
columns={tableColumn} columns={tableColumn}
data-testid="databaseSchema-tables" data-testid="databaseSchema-tables"
dataSource={tableData} dataSource={localTableData}
loading={tableDataLoading} loading={tableDataLoading}
locale={{ locale={{
emptyText: ( emptyText: (

View File

@ -209,7 +209,7 @@ function DatabaseVersionPage() {
/> />
</Col> </Col>
<Col span={24}> <Col span={24}>
<DatabaseSchemaTable /> <DatabaseSchemaTable isVersionPage />
</Col> </Col>
</Row> </Row>
</Col> </Col>

View File

@ -13,9 +13,11 @@
import { Col, Row, Space, Switch, Table, Typography } from 'antd'; import { Col, Row, Space, Switch, Table, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table'; import { ColumnsType } from 'antd/lib/table';
import { AxiosError } from 'axios';
import { compare } from 'fast-json-patch';
import { isUndefined } from 'lodash'; import { isUndefined } from 'lodash';
import { EntityTags, ServiceTypes } from 'Models'; 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 { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import DescriptionV1 from '../../components/common/EntityDescription/DescriptionV1'; 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 { NextPreviousProps } from '../../components/common/NextPrevious/NextPrevious.interface';
import ResizablePanels from '../../components/common/ResizablePanels/ResizablePanels'; import ResizablePanels from '../../components/common/ResizablePanels/ResizablePanels';
import EntityRightPanel from '../../components/Entity/EntityRightPanel/EntityRightPanel'; 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 { COMMON_RESIZABLE_PANEL_CONFIG } from '../../constants/ResizablePanel.constants';
import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider';
import { OperationPermission } from '../../context/PermissionProvider/PermissionProvider.interface'; import { OperationPermission } from '../../context/PermissionProvider/PermissionProvider.interface';
import { EntityType } from '../../enums/entity.enum'; import { EntityType } from '../../enums/entity.enum';
import { DatabaseService } from '../../generated/entity/services/databaseService'; import { DatabaseService } from '../../generated/entity/services/databaseService';
@ -33,10 +37,14 @@ import { Paging } from '../../generated/type/paging';
import { UsePagingInterface } from '../../hooks/paging/usePaging'; import { UsePagingInterface } from '../../hooks/paging/usePaging';
import { useFqn } from '../../hooks/useFqn'; import { useFqn } from '../../hooks/useFqn';
import { ServicesType } from '../../interface/service.interface'; import { ServicesType } from '../../interface/service.interface';
import { getServiceMainTabColumns } from '../../utils/ServiceMainTabContentUtils'; import {
callServicePatchAPI,
getServiceMainTabColumns,
} from '../../utils/ServiceMainTabContentUtils';
import { getEntityTypeFromServiceCategory } from '../../utils/ServiceUtils'; import { getEntityTypeFromServiceCategory } from '../../utils/ServiceUtils';
import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils'; import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils';
import { createTagObject } from '../../utils/TagsUtils'; import { createTagObject } from '../../utils/TagsUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import { ServicePageData } from './ServiceDetailsPage'; import { ServicePageData } from './ServiceDetailsPage';
interface ServiceMainTabContentProps { interface ServiceMainTabContentProps {
@ -53,6 +61,7 @@ interface ServiceMainTabContentProps {
pagingHandler: NextPreviousProps['pagingHandler']; pagingHandler: NextPreviousProps['pagingHandler'];
saveUpdatedServiceData: (updatedData: ServicesType) => Promise<void>; saveUpdatedServiceData: (updatedData: ServicesType) => Promise<void>;
pagingInfo: UsePagingInterface; pagingInfo: UsePagingInterface;
isVersionPage?: boolean;
} }
function ServiceMainTabContent({ function ServiceMainTabContent({
@ -69,6 +78,7 @@ function ServiceMainTabContent({
serviceDetails, serviceDetails,
saveUpdatedServiceData, saveUpdatedServiceData,
pagingInfo, pagingInfo,
isVersionPage = false,
}: Readonly<ServiceMainTabContentProps>) { }: Readonly<ServiceMainTabContentProps>) {
const { t } = useTranslation(); const { t } = useTranslation();
const { serviceCategory } = useParams<{ const { serviceCategory } = useParams<{
@ -76,7 +86,10 @@ function ServiceMainTabContent({
}>(); }>();
const { fqn: serviceFQN } = useFqn(); const { fqn: serviceFQN } = useFqn();
const { permissions } = usePermissionProvider();
const [isEdit, setIsEdit] = useState(false); const [isEdit, setIsEdit] = useState(false);
const [pageData, setPageData] = useState<ServicePageData[]>([]);
const tier = getTierTags(serviceDetails?.tags ?? []); const tier = getTierTags(serviceDetails?.tags ?? []);
const tags = getTagsWithoutTier(serviceDetails?.tags ?? []); const tags = getTagsWithoutTier(serviceDetails?.tags ?? []);
@ -131,9 +144,69 @@ function ServiceMainTabContent({
setIsEdit(false); setIsEdit(false);
}; };
const handleDisplayNameUpdate = useCallback(
async (entityData: EntityName, id?: string) => {
try {
const pageDataDetails = pageData.find((data) => data.id === id);
if (!pageDataDetails) {
return;
}
const updatedData = {
...pageDataDetails,
displayName: entityData.displayName || undefined,
};
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<ServicePageData> = useMemo( const tableColumn: ColumnsType<ServicePageData> = useMemo(
() => getServiceMainTabColumns(serviceCategory), () =>
[serviceCategory] getServiceMainTabColumns(
serviceCategory,
editDisplayNamePermission,
handleDisplayNameUpdate
),
[serviceCategory, handleDisplayNameUpdate, editDisplayNamePermission]
); );
const entityType = useMemo( const entityType = useMemo(
@ -160,6 +233,10 @@ function ServiceMainTabContent({
[servicePermission, serviceDetails] [servicePermission, serviceDetails]
); );
useEffect(() => {
setPageData(data);
}, [data]);
return ( return (
<Row gutter={[0, 16]} wrap={false}> <Row gutter={[0, 16]} wrap={false}>
<Col className="tab-content-height-with-resizable-panel" span={24}> <Col className="tab-content-height-with-resizable-panel" span={24}>
@ -210,7 +287,7 @@ function ServiceMainTabContent({
bordered bordered
columns={tableColumn} columns={tableColumn}
data-testid="service-children-table" data-testid="service-children-table"
dataSource={data} dataSource={pageData}
locale={{ locale={{
emptyText: <ErrorPlaceHolder className="m-y-md" />, emptyText: <ErrorPlaceHolder className="m-y-md" />,
}} }}

View File

@ -17,44 +17,51 @@ import { t } from 'i18next';
import { isUndefined } from 'lodash'; import { isUndefined } from 'lodash';
import { ServiceTypes } from 'Models'; import { ServiceTypes } from 'Models';
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import DisplayName from '../components/common/DisplayName/DisplayName';
import { OwnerLabel } from '../components/common/OwnerLabel/OwnerLabel.component'; import { OwnerLabel } from '../components/common/OwnerLabel/OwnerLabel.component';
import RichTextEditorPreviewer from '../components/common/RichTextEditor/RichTextEditorPreviewer'; import RichTextEditorPreviewer from '../components/common/RichTextEditor/RichTextEditorPreviewer';
import { EntityName } from '../components/Modals/EntityNameModal/EntityNameModal.interface';
import TagsViewer from '../components/Tag/TagsViewer/TagsViewer'; import TagsViewer from '../components/Tag/TagsViewer/TagsViewer';
import { NO_DATA_PLACEHOLDER } from '../constants/constants'; import { NO_DATA_PLACEHOLDER } from '../constants/constants';
import { ServiceCategory } from '../enums/service.enum'; import { ServiceCategory } from '../enums/service.enum';
import { Database } from '../generated/entity/data/database'; import { Database } from '../generated/entity/data/database';
import { Pipeline } from '../generated/entity/data/pipeline'; import { Pipeline } from '../generated/entity/data/pipeline';
import { ServicePageData } from '../pages/ServiceDetailsPage/ServiceDetailsPage'; import { ServicePageData } from '../pages/ServiceDetailsPage/ServiceDetailsPage';
import { getEntityName } from './EntityUtils'; 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 { getLinkForFqn } from './ServiceUtils';
import { getUsagePercentile } from './TableUtils'; import { getUsagePercentile } from './TableUtils';
export const getServiceMainTabColumns = ( export const getServiceMainTabColumns = (
serviceCategory: ServiceTypes serviceCategory: ServiceTypes,
editDisplayNamePermission?: boolean,
handleDisplayNameUpdate?: (
entityData: EntityName,
id?: string
) => Promise<void>
): ColumnsType<ServicePageData> => [ ): ColumnsType<ServicePageData> => [
{ {
title: t('label.name'), title: t('label.name'),
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
width: 280, width: 280,
render: (_, record: ServicePageData) => { render: (_, record: ServicePageData) => (
return ( <DisplayName
<Link allowRename={editDisplayNamePermission}
data-testid={record.name} displayName={record.displayName}
to={getLinkForFqn(serviceCategory, record.fullyQualifiedName ?? '')}> id={record.id}
<Typography.Paragraph key={record.id}
data-testid="child-asset-name-link" link={getLinkForFqn(serviceCategory, record.fullyQualifiedName ?? '')}
ellipsis={{ name={record.name}
rows: 2, onEditDisplayName={handleDisplayNameUpdate}
tooltip: true, />
}} ),
style={{ width: 280, color: 'inherit' }}>
{getEntityName(record)}
</Typography.Paragraph>
</Link>
);
},
}, },
{ {
title: t('label.description'), title: t('label.description'),
@ -123,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:
return;
}
};