mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-02 03:29:03 +00:00
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:
parent
cb33f274fc
commit
9d37ff0e34
@ -13,4 +13,5 @@
|
||||
|
||||
export interface DatabaseSchemaTableProps {
|
||||
isDatabaseDeleted?: boolean;
|
||||
isVersionPage?: boolean;
|
||||
}
|
||||
|
||||
@ -11,45 +11,70 @@
|
||||
* 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 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<DatabaseSchemaTableProps>) => {
|
||||
const { fqn: decodedDatabaseFQN } = useFqn();
|
||||
const history = useHistory();
|
||||
const location = useCustomLocation();
|
||||
const { permissions } = usePermissionProvider();
|
||||
|
||||
const [schemas, setSchemas] = useState<DatabaseSchema[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [showDeletedSchemas, setShowDeletedSchemas] = useState<boolean>(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 +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(() => {
|
||||
fetchDatabaseSchema();
|
||||
}, [decodedDatabaseFQN, pageSize, showDeletedSchemas, isDatabaseDeleted]);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
@ -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,48 @@ function SchemaTablesTab({
|
||||
pagingInfo,
|
||||
}: Readonly<SchemaTablesTabProps>) {
|
||||
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(
|
||||
() => [
|
||||
@ -79,17 +127,18 @@ function SchemaTablesTab({
|
||||
width: 500,
|
||||
render: (_, record: Table) => {
|
||||
return (
|
||||
<div className="d-inline-flex w-max-90">
|
||||
<Link
|
||||
className="break-word"
|
||||
data-testid={record.name}
|
||||
to={entityUtilClassBase.getEntityLink(
|
||||
EntityType.TABLE,
|
||||
record.fullyQualifiedName as string
|
||||
)}>
|
||||
{getEntityName(record)}
|
||||
</Link>
|
||||
</div>
|
||||
<DisplayName
|
||||
allowRename={allowEditDisplayNamePermission}
|
||||
displayName={record.displayName}
|
||||
id={record.id}
|
||||
key={record.id}
|
||||
link={entityUtilClassBase.getEntityLink(
|
||||
EntityType.TABLE,
|
||||
record.fullyQualifiedName as string
|
||||
)}
|
||||
name={record.name}
|
||||
onEditDisplayName={handleDisplayNameUpdate}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
@ -105,7 +154,7 @@ function SchemaTablesTab({
|
||||
),
|
||||
},
|
||||
],
|
||||
[]
|
||||
[handleDisplayNameUpdate, allowEditDisplayNamePermission]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -158,7 +207,7 @@ function SchemaTablesTab({
|
||||
bordered
|
||||
columns={tableColumn}
|
||||
data-testid="databaseSchema-tables"
|
||||
dataSource={tableData}
|
||||
dataSource={localTableData}
|
||||
loading={tableDataLoading}
|
||||
locale={{
|
||||
emptyText: (
|
||||
|
||||
@ -209,7 +209,7 @@ function DatabaseVersionPage() {
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<DatabaseSchemaTable />
|
||||
<DatabaseSchemaTable isVersionPage />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
|
||||
@ -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,14 @@ import { Paging } from '../../generated/type/paging';
|
||||
import { UsePagingInterface } from '../../hooks/paging/usePaging';
|
||||
import { useFqn } from '../../hooks/useFqn';
|
||||
import { ServicesType } from '../../interface/service.interface';
|
||||
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';
|
||||
import { showErrorToast } from '../../utils/ToastUtils';
|
||||
import { ServicePageData } from './ServiceDetailsPage';
|
||||
|
||||
interface ServiceMainTabContentProps {
|
||||
@ -53,6 +61,7 @@ interface ServiceMainTabContentProps {
|
||||
pagingHandler: NextPreviousProps['pagingHandler'];
|
||||
saveUpdatedServiceData: (updatedData: ServicesType) => Promise<void>;
|
||||
pagingInfo: UsePagingInterface;
|
||||
isVersionPage?: boolean;
|
||||
}
|
||||
|
||||
function ServiceMainTabContent({
|
||||
@ -69,6 +78,7 @@ function ServiceMainTabContent({
|
||||
serviceDetails,
|
||||
saveUpdatedServiceData,
|
||||
pagingInfo,
|
||||
isVersionPage = false,
|
||||
}: Readonly<ServiceMainTabContentProps>) {
|
||||
const { t } = useTranslation();
|
||||
const { serviceCategory } = useParams<{
|
||||
@ -76,7 +86,10 @@ function ServiceMainTabContent({
|
||||
}>();
|
||||
|
||||
const { fqn: serviceFQN } = useFqn();
|
||||
const { permissions } = usePermissionProvider();
|
||||
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [pageData, setPageData] = useState<ServicePageData[]>([]);
|
||||
|
||||
const tier = getTierTags(serviceDetails?.tags ?? []);
|
||||
const tags = getTagsWithoutTier(serviceDetails?.tags ?? []);
|
||||
@ -131,9 +144,69 @@ function ServiceMainTabContent({
|
||||
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(
|
||||
() => getServiceMainTabColumns(serviceCategory),
|
||||
[serviceCategory]
|
||||
() =>
|
||||
getServiceMainTabColumns(
|
||||
serviceCategory,
|
||||
editDisplayNamePermission,
|
||||
handleDisplayNameUpdate
|
||||
),
|
||||
[serviceCategory, handleDisplayNameUpdate, editDisplayNamePermission]
|
||||
);
|
||||
|
||||
const entityType = useMemo(
|
||||
@ -160,6 +233,10 @@ function ServiceMainTabContent({
|
||||
[servicePermission, serviceDetails]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setPageData(data);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<Row gutter={[0, 16]} wrap={false}>
|
||||
<Col className="tab-content-height-with-resizable-panel" span={24}>
|
||||
@ -210,7 +287,7 @@ function ServiceMainTabContent({
|
||||
bordered
|
||||
columns={tableColumn}
|
||||
data-testid="service-children-table"
|
||||
dataSource={data}
|
||||
dataSource={pageData}
|
||||
locale={{
|
||||
emptyText: <ErrorPlaceHolder className="m-y-md" />,
|
||||
}}
|
||||
|
||||
@ -17,44 +17,51 @@ 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 { OwnerLabel } from '../components/common/OwnerLabel/OwnerLabel.component';
|
||||
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';
|
||||
import { Database } from '../generated/entity/data/database';
|
||||
import { Pipeline } from '../generated/entity/data/pipeline';
|
||||
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 { getUsagePercentile } from './TableUtils';
|
||||
|
||||
export const getServiceMainTabColumns = (
|
||||
serviceCategory: ServiceTypes
|
||||
serviceCategory: ServiceTypes,
|
||||
editDisplayNamePermission?: boolean,
|
||||
handleDisplayNameUpdate?: (
|
||||
entityData: EntityName,
|
||||
id?: string
|
||||
) => Promise<void>
|
||||
): ColumnsType<ServicePageData> => [
|
||||
{
|
||||
title: t('label.name'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 280,
|
||||
render: (_, record: ServicePageData) => {
|
||||
return (
|
||||
<Link
|
||||
data-testid={record.name}
|
||||
to={getLinkForFqn(serviceCategory, record.fullyQualifiedName ?? '')}>
|
||||
<Typography.Paragraph
|
||||
data-testid="child-asset-name-link"
|
||||
ellipsis={{
|
||||
rows: 2,
|
||||
tooltip: true,
|
||||
}}
|
||||
style={{ width: 280, color: 'inherit' }}>
|
||||
{getEntityName(record)}
|
||||
</Typography.Paragraph>
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
render: (_, record: ServicePageData) => (
|
||||
<DisplayName
|
||||
allowRename={editDisplayNamePermission}
|
||||
displayName={record.displayName}
|
||||
id={record.id}
|
||||
key={record.id}
|
||||
link={getLinkForFqn(serviceCategory, record.fullyQualifiedName ?? '')}
|
||||
name={record.name}
|
||||
onEditDisplayName={handleDisplayNameUpdate}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user