Skip to content

Commit

Permalink
Feat: edit display name (#18720)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
pranita09 and ShaileshParmar11 authored Nov 28, 2024
1 parent cb33f27 commit 9d37ff0
Show file tree
Hide file tree
Showing 10 changed files with 550 additions and 45 deletions.
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,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(
Expand Down Expand Up @@ -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]);
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,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;
}
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
Loading

0 comments on commit 9d37ff0

Please sign in to comment.