Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: edit display name #18720

Merged
merged 11 commits into from
Nov 28, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@

export interface DatabaseSchemaTableProps {
isDatabaseDeleted?: boolean;
isVersionPage?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<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]);
Ashish8689 marked this conversation as resolved.
Show resolved Hide resolved

const searchValue = useMemo(() => {
const param = location.search;
const searchData = QueryString.parse(
Expand Down Expand Up @@ -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'));
Ashish8689 marked this conversation as resolved.
Show resolved Hide resolved
}
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]
Ashish8689 marked this conversation as resolved.
Show resolved Hide resolved
);

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[]) =>
!isUndefined(owners) && owners.length > 0 ? (
aniketkatkar97 marked this conversation as resolved.
Show resolved Hide resolved
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]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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 }) => (
<span {...props}>{children}</span>
)),
}));

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(() => (
<div data-testid="entity-name-modal">Mocked Modal</div>
)),
};
});
aniketkatkar97 marked this conversation as resolved.
Show resolved Hide resolved

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 = 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(
<MemoryRouter>
<DisplayName {...mockProps} displayName={undefined} />
</MemoryRouter>
);

const nameField = await screen.getByTestId('column-name');
aniketkatkar97 marked this conversation as resolved.
Show resolved Hide resolved

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();
});
});
});
Loading
Loading