feat(ui): supported latest ui for the database page (#12277)

* supported latest ui for the database page

* fix unit test
This commit is contained in:
Ashish Gupta 2023-07-06 12:29:45 +05:30 committed by GitHub
parent 9ae3407be0
commit f8ff645e5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 334 additions and 540 deletions

View File

@ -37,6 +37,7 @@ import { EntityTabs, EntityType } from 'enums/entity.enum';
import { Container } from 'generated/entity/data/container'; import { Container } from 'generated/entity/data/container';
import { Dashboard } from 'generated/entity/data/dashboard'; import { Dashboard } from 'generated/entity/data/dashboard';
import { DashboardDataModel } from 'generated/entity/data/dashboardDataModel'; import { DashboardDataModel } from 'generated/entity/data/dashboardDataModel';
import { Database } from 'generated/entity/data/database';
import { Mlmodel } from 'generated/entity/data/mlmodel'; import { Mlmodel } from 'generated/entity/data/mlmodel';
import { Pipeline } from 'generated/entity/data/pipeline'; import { Pipeline } from 'generated/entity/data/pipeline';
import { Table } from 'generated/entity/data/table'; import { Table } from 'generated/entity/data/table';
@ -56,6 +57,7 @@ import { getCurrentUserId, getEntityDetailLink } from 'utils/CommonUtils';
import { import {
getBreadcrumbForEntitiesWithServiceOnly, getBreadcrumbForEntitiesWithServiceOnly,
getBreadcrumbForTable, getBreadcrumbForTable,
getEntityBreadcrumbs,
getEntityFeedLink, getEntityFeedLink,
getEntityName, getEntityName,
} from 'utils/EntityUtils'; } from 'utils/EntityUtils';
@ -66,6 +68,7 @@ import { showErrorToast } from 'utils/ToastUtils';
import { import {
DataAssetHeaderInfo, DataAssetHeaderInfo,
DataAssetsHeaderProps, DataAssetsHeaderProps,
DataAssetType,
} from './DataAssetsHeader.interface'; } from './DataAssetsHeader.interface';
export const ExtraInfoLabel = ({ export const ExtraInfoLabel = ({
@ -108,6 +111,7 @@ export const ExtraInfoLink = ({
); );
export const DataAssetsHeader = ({ export const DataAssetsHeader = ({
allowSoftDelete = true,
dataAsset, dataAsset,
onOwnerUpdate, onOwnerUpdate,
onTierUpdate, onTierUpdate,
@ -115,6 +119,7 @@ export const DataAssetsHeader = ({
onVersionClick, onVersionClick,
onFollowClick, onFollowClick,
entityType, entityType,
isRecursiveDelete,
onRestoreDataAsset, onRestoreDataAsset,
onDisplayNameUpdate, onDisplayNameUpdate,
}: DataAssetsHeaderProps) => { }: DataAssetsHeaderProps) => {
@ -133,13 +138,23 @@ export const DataAssetsHeader = ({
); );
const [copyTooltip, setCopyTooltip] = useState<string>(); const [copyTooltip, setCopyTooltip] = useState<string>();
const excludeEntityService = [EntityType.DATABASE].includes(entityType);
const hasFollowers = 'followers' in dataAsset;
const { entityName, tier, isFollowing, version, followers } = useMemo( const { entityName, tier, isFollowing, version, followers } = useMemo(
() => ({ () => ({
isFollowing: dataAsset.followers?.some(({ id }) => id === USERId), isFollowing: hasFollowers
? (dataAsset as DataAssetType).followers?.some(
({ id }) => id === USERId
)
: false,
followers: hasFollowers
? (dataAsset as DataAssetType).followers?.length
: 0,
tier: getTierTags(dataAsset.tags ?? []), tier: getTierTags(dataAsset.tags ?? []),
entityName: getEntityName(dataAsset), entityName: getEntityName(dataAsset),
version: dataAsset.version, version: dataAsset.version,
followers: dataAsset.followers?.length,
}), }),
[dataAsset, USERId] [dataAsset, USERId]
); );
@ -363,6 +378,16 @@ export const DataAssetsHeader = ({
break; break;
case EntityType.DATABASE:
const databaseDetails = dataAsset as Database;
returnData.breadcrumbs = getEntityBreadcrumbs(
databaseDetails,
EntityType.DATABASE
);
break;
case EntityType.TABLE: case EntityType.TABLE:
default: default:
const tableDetails = dataAsset as Table; const tableDetails = dataAsset as Table;
@ -492,27 +517,36 @@ export const DataAssetsHeader = ({
<Space className="items-end w-full" direction="vertical" size={16}> <Space className="items-end w-full" direction="vertical" size={16}>
<Space> <Space>
<ButtonGroup size="small"> <ButtonGroup size="small">
<Button {!excludeEntityService && (
className="w-16 p-0" <>
icon={<Icon component={TaskOpenIcon} />} <Button
onClick={handleOpenTaskClick}> className="w-16 p-0"
<Typography.Text>{taskCount}</Typography.Text> icon={<Icon component={TaskOpenIcon} />}
</Button> onClick={handleOpenTaskClick}>
<Button <Typography.Text>{taskCount}</Typography.Text>
className="w-16 p-0" </Button>
icon={<Icon component={VersionIcon} />}
onClick={onVersionClick}> <Button
<Typography.Text>{version}</Typography.Text> className="w-16 p-0"
</Button> icon={<Icon component={VersionIcon} />}
<Button onClick={onVersionClick}>
className="w-16 p-0" <Typography.Text>{version}</Typography.Text>
data-testid="entity-follow-button" </Button>
icon={
<Icon component={isFollowing ? StarFilledIcon : StarIcon} /> <Button
} className="w-16 p-0"
onClick={onFollowClick}> data-testid="entity-follow-button"
<Typography.Text>{followers}</Typography.Text> icon={
</Button> <Icon
component={isFollowing ? StarFilledIcon : StarIcon}
/>
}
onClick={onFollowClick}>
<Typography.Text>{followers}</Typography.Text>
</Button>
</>
)}
<Tooltip <Tooltip
open={!isEmpty(copyTooltip)} open={!isEmpty(copyTooltip)}
placement="bottomRight" placement="bottomRight"
@ -523,7 +557,7 @@ export const DataAssetsHeader = ({
/> />
</Tooltip> </Tooltip>
<ManageButton <ManageButton
allowSoftDelete={!dataAsset.deleted} allowSoftDelete={!dataAsset.deleted && allowSoftDelete}
canDelete={permissions.Delete} canDelete={permissions.Delete}
deleted={dataAsset.deleted} deleted={dataAsset.deleted}
displayName={dataAsset.displayName} displayName={dataAsset.displayName}
@ -534,6 +568,7 @@ export const DataAssetsHeader = ({
entityId={dataAsset.id} entityId={dataAsset.id}
entityName={entityName} entityName={entityName}
entityType={entityType} entityType={entityType}
isRecursiveDelete={isRecursiveDelete}
onAnnouncementClick={ onAnnouncementClick={
permissions?.EditAll permissions?.EditAll
? () => setIsAnnouncementDrawer(true) ? () => setIsAnnouncementDrawer(true)

View File

@ -17,6 +17,7 @@ import { EntityType } from 'enums/entity.enum';
import { Container } from 'generated/entity/data/container'; import { Container } from 'generated/entity/data/container';
import { Dashboard } from 'generated/entity/data/dashboard'; import { Dashboard } from 'generated/entity/data/dashboard';
import { DashboardDataModel } from 'generated/entity/data/dashboardDataModel'; import { DashboardDataModel } from 'generated/entity/data/dashboardDataModel';
import { Database } from 'generated/entity/data/database';
import { Mlmodel } from 'generated/entity/data/mlmodel'; import { Mlmodel } from 'generated/entity/data/mlmodel';
import { Pipeline } from 'generated/entity/data/pipeline'; import { Pipeline } from 'generated/entity/data/pipeline';
import { Table } from 'generated/entity/data/table'; import { Table } from 'generated/entity/data/table';
@ -24,21 +25,23 @@ import { Topic } from 'generated/entity/data/topic';
import { EntityReference } from 'generated/entity/type'; import { EntityReference } from 'generated/entity/type';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
export type DataAssetsSourceMapping = { export type DataAssetType =
[EntityType.TABLE]: Table; | Table
[EntityType.TOPIC]: Topic; | Topic
[EntityType.DASHBOARD]: Dashboard; | Dashboard
[EntityType.PIPELINE]: Pipeline; | Pipeline
[EntityType.MLMODEL]: Mlmodel; | Mlmodel
[EntityType.CONTAINER]: Container; | Container
}; | DashboardDataModel;
export type DataAssetsHeaderProps = { export type DataAssetsHeaderProps = {
permissions: OperationPermission; permissions: OperationPermission;
allowSoftDelete?: boolean;
isRecursiveDelete?: boolean;
onTierUpdate: (tier?: string) => Promise<void>; onTierUpdate: (tier?: string) => Promise<void>;
onOwnerUpdate: (owner?: EntityReference) => Promise<void>; onOwnerUpdate: (owner?: EntityReference) => Promise<void>;
onVersionClick: () => void; onVersionClick?: () => void;
onFollowClick: () => Promise<void>; onFollowClick?: () => Promise<void>;
onRestoreDataAsset: () => Promise<void>; onRestoreDataAsset: () => Promise<void>;
onDisplayNameUpdate: (data: EntityName) => Promise<void>; onDisplayNameUpdate: (data: EntityName) => Promise<void>;
} & ( } & (
@ -49,6 +52,7 @@ export type DataAssetsHeaderProps = {
| DataAssetMlmodel | DataAssetMlmodel
| DataAssetContainer | DataAssetContainer
| DataAssetDashboardDataModel | DataAssetDashboardDataModel
| DataAssetDatabase
); );
export interface DataAssetTable { export interface DataAssetTable {
@ -85,6 +89,11 @@ export interface DataAssetDashboardDataModel {
entityType: EntityType.DASHBOARD_DATA_MODEL; entityType: EntityType.DASHBOARD_DATA_MODEL;
} }
export interface DataAssetDatabase {
dataAsset: Database;
entityType: EntityType.DATABASE;
}
export interface DataAssetHeaderInfo { export interface DataAssetHeaderInfo {
extraInfo: ReactNode; extraInfo: ReactNode;
breadcrumbs: TitleBreadcrumbProps['titleLinks']; breadcrumbs: TitleBreadcrumbProps['titleLinks'];

View File

@ -128,7 +128,8 @@ export type EntityWithServices =
| Pipeline | Pipeline
| Mlmodel | Mlmodel
| Container | Container
| DashboardDataModel; | DashboardDataModel
| Database;
export interface EntityDetailsObjectInterface { export interface EntityDetailsObjectInterface {
details: SearchedDataProps['data'][number]['_source']; details: SearchedDataProps['data'][number]['_source'];

View File

@ -39,16 +39,6 @@ const mockDatabase = {
}, },
}; };
const mockServiceData = {
id: 'bc13e95f-83ac-458a-9528-f4ca26657568',
type: 'databaseService',
name: 'bigquery_gcp',
description: '',
deleted: false,
href: 'http://localhost:8585/api/v1/services/databaseServices/bc13e95f-83ac-458a-9528-f4ca26657568',
jdbc: { driverClass: 'jdbc', connectionUrl: 'jdbc://localhost' },
};
const mockSchemaData = { const mockSchemaData = {
data: [ data: [
{ {
@ -100,59 +90,6 @@ const mockSchemaData = {
paging: { after: 'ZMbpLOqQQsREk_7DmEOr', total: 12 }, paging: { after: 'ZMbpLOqQQsREk_7DmEOr', total: 12 },
}; };
const mockAllFeeds = {
data: [
{
id: 'ac2e6128-9f23-4f28-acf8-31d50b06f8cc',
type: 'Task',
href: 'http://localhost:8585/api/v1/feed/ac2e6128-9f23-4f28-acf8-31d50b06f8cc',
threadTs: 1664445686074,
about: '<#E::table::sample_data.ecommerce_db.shopify.raw_order::tags>',
entityId: 'c514ca18-2ea4-44b1-aa06-0c66bc0cd355',
createdBy: 'bharatdussa',
updatedAt: 1664445691373,
updatedBy: 'bharatdussa',
resolved: false,
message: 'Update tags for table',
postsCount: 1,
posts: [
{
id: 'd497bea2-0bfe-4a9f-9ce6-d560129fef4a',
message: 'Resolved the Task with Tag(s) - PersonalData.Personal',
postTs: 1664445691368,
from: 'bharatdussa',
reactions: [],
},
],
reactions: [],
task: {
id: 11,
type: 'UpdateTag',
assignees: [
{
id: 'f187364d-114c-4426-b941-baf6a15f70e4',
type: 'user',
name: 'bharatdussa',
fullyQualifiedName: 'bharatdussa',
displayName: 'Bharat Dussa',
deleted: false,
},
],
status: 'Closed',
closedBy: 'bharatdussa',
closedAt: 1664445691340,
oldValue:
'[{"tagFQN":"PersonalData.Personal","description":"","source":"Classification","labelType":"Manual","state":"Suggested"},]',
suggestion:
'[{"tagFQN":"PersonalData.Personal","description":"","source":"Classification","labelType":"Manual","state":"Suggested"},]',
newValue:
'[{"tagFQN":"PersonalData.Personal","description":"","source":"Classification","labelType":"Manual","state":"Suggested"},]',
},
},
],
paging: { after: 'MTY2NDQ0NDcyODY1MA==', total: 134 },
};
const mockFeedCount = { const mockFeedCount = {
totalCount: 6, totalCount: 6,
counts: [ counts: [
@ -209,6 +146,19 @@ jest.mock('react-router-dom', () => ({
}), }),
})); }));
jest.mock(
'components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider',
() => ({
useActivityFeedProvider: jest.fn().mockImplementation(() => ({
postFeed: jest.fn(),
deleteFeed: jest.fn(),
updateFeed: jest.fn(),
})),
__esModule: true,
default: 'ActivityFeedProvider',
})
);
jest.mock('../../AppState', () => { jest.mock('../../AppState', () => {
return jest.fn().mockReturnValue({ return jest.fn().mockReturnValue({
inPageSearchText: '', inPageSearchText: '',
@ -229,60 +179,24 @@ jest.mock('rest/databaseAPI', () => ({
})); }));
jest.mock('rest/feedsAPI', () => ({ jest.mock('rest/feedsAPI', () => ({
getAllFeeds: jest
.fn()
.mockImplementation(() => Promise.resolve(mockAllFeeds)),
getFeedCount: jest getFeedCount: jest
.fn() .fn()
.mockImplementation(() => Promise.resolve(mockFeedCount)), .mockImplementation(() => Promise.resolve(mockFeedCount)),
postFeedById: jest.fn().mockImplementation(() => Promise.resolve({})),
postThread: jest.fn().mockImplementation(() => Promise.resolve({})), postThread: jest.fn().mockImplementation(() => Promise.resolve({})),
})); }));
jest.mock('rest/serviceAPI', () => ({
getServiceById: jest
.fn()
.mockImplementation(() => Promise.resolve({ data: mockServiceData })),
}));
jest.mock('../../utils/TableUtils', () => ({ jest.mock('../../utils/TableUtils', () => ({
getOwnerFromId: jest.fn().mockReturnValue({
name: 'owner',
id: 'string',
type: 'user',
}),
getUsagePercentile: jest.fn().mockReturnValue('Medium - 45th pctile'), getUsagePercentile: jest.fn().mockReturnValue('Medium - 45th pctile'),
getTierTags: jest.fn().mockImplementation(() => ({})), getTierTags: jest.fn().mockImplementation(() => ({})),
getTagsWithoutTier: jest.fn().mockImplementation(() => []), getTagsWithoutTier: jest.fn().mockImplementation(() => []),
})); }));
jest.mock('../../utils/CommonUtils', () => ({
getCurrentUserId: jest
.fn()
.mockReturnValue('5d5ca778-8bee-4ea0-bcb6-b17d92f7ef96'),
isEven: jest.fn().mockReturnValue(true),
getEntityName: jest.fn().mockReturnValue('entityname'),
}));
jest.mock('components/Tag/Tags/tags', () => {
return jest.fn().mockReturnValue(<span>Tag</span>);
});
jest.mock('components/common/next-previous/NextPrevious', () => { jest.mock('components/common/next-previous/NextPrevious', () => {
return jest.fn().mockReturnValue(<div>NextPrevious</div>); return jest.fn().mockReturnValue(<div>NextPrevious</div>);
}); });
jest.mock( jest.mock('components/Tag/TagsContainerV2/TagsContainerV2', () => {
'components/common/title-breadcrumb/title-breadcrumb.component', return jest.fn().mockReturnValue(<div>TagsContainerV2</div>);
() => {
return jest.fn().mockReturnValue(<div>TitleBreadcrumb</div>);
}
);
jest.mock('components/FeedEditor/FeedEditor', () => {
return jest.fn().mockReturnValue(<p>FeedEditor</p>);
}); });
jest.mock('../../utils/TagsUtils', () => ({ jest.mock('../../utils/TagsUtils', () => ({
@ -309,37 +223,36 @@ jest.mock(
}) })
); );
jest.mock('components/common/description/Description', () => { jest.mock('components/common/description/DescriptionV1', () => {
return jest.fn().mockReturnValue(<p>Description</p>); return jest.fn().mockReturnValue(<p>Description</p>);
}); });
jest.mock('components/common/EntitySummaryDetails/EntitySummaryDetails', () => {
return jest
.fn()
.mockReturnValue(
<p data-testid="entity-summary-details">EntitySummaryDetails component</p>
);
});
jest.mock('components/common/DeleteWidget/DeleteWidgetModal', () => {
return jest
.fn()
.mockReturnValue(
<p data-testid="delete-entity">DeleteWidgetModal component</p>
);
});
jest.mock('components/MyData/LeftSidebar/LeftSidebar.component', () =>
jest.fn().mockReturnValue(<p>Sidebar</p>)
);
jest.mock('components/containers/PageLayoutV1', () => { jest.mock('components/containers/PageLayoutV1', () => {
return jest.fn().mockImplementation(({ children }) => children); return jest.fn().mockImplementation(({ children }) => children);
}); });
jest.mock('components/Entity/EntityHeader/EntityHeader.component', () => ({ jest.mock(
EntityHeader: jest.fn().mockImplementation(() => <p>EntityHeader</p>), 'components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component',
})); () => {
return jest.fn().mockReturnValue(<p>ActivityFeedTab</p>);
}
);
jest.mock(
'components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel',
() => {
return jest.fn().mockReturnValue(<p>ActivityThreadPanel</p>);
}
);
jest.mock(
'components/DataAssets/DataAssetsHeader/DataAssetsHeader.component',
() => ({
DataAssetsHeader: jest
.fn()
.mockImplementation(() => <p>DataAssetsHeader</p>),
})
);
describe('Test DatabaseDetails page', () => { describe('Test DatabaseDetails page', () => {
it('Component should render', async () => { it('Component should render', async () => {
@ -347,11 +260,8 @@ describe('Test DatabaseDetails page', () => {
wrapper: MemoryRouter, wrapper: MemoryRouter,
}); });
const entityHeader = await findByText(container, 'EntityHeader'); const entityHeader = await findByText(container, 'DataAssetsHeader');
const descriptionContainer = await findByTestId( const descriptionContainer = await findByText(container, 'Description');
container,
'description-container'
);
const databaseTable = await findByTestId( const databaseTable = await findByTestId(
container, container,
'database-databaseSchemas' 'database-databaseSchemas'
@ -421,11 +331,8 @@ describe('Test DatabaseDetails page', () => {
wrapper: MemoryRouter, wrapper: MemoryRouter,
}); });
const entityHeader = await findByText(container, 'EntityHeader'); const entityHeader = await findByText(container, 'DataAssetsHeader');
const descriptionContainer = await findByTestId( const descriptionContainer = await findByText(container, 'Description');
container,
'description-container'
);
const databaseTable = await findByTestId( const databaseTable = await findByTestId(
container, container,
'database-databaseSchemas' 'database-databaseSchemas'

View File

@ -11,16 +11,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { import { Col, Row, Space, Switch, Table, Tabs, Typography } from 'antd';
Col,
Row,
Skeleton,
Space,
Switch,
Table,
Tabs,
Typography,
} from 'antd';
import { ColumnsType } from 'antd/lib/table'; import { ColumnsType } from 'antd/lib/table';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import ActivityFeedProvider, { import ActivityFeedProvider, {
@ -29,14 +20,11 @@ import ActivityFeedProvider, {
import { ActivityFeedTab } from 'components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import { ActivityFeedTab } from 'components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component';
import ActivityThreadPanel from 'components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import ActivityThreadPanel from 'components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel';
import DescriptionV1 from 'components/common/description/DescriptionV1'; import DescriptionV1 from 'components/common/description/DescriptionV1';
import ManageButton from 'components/common/entityPageInfo/ManageButton/ManageButton';
import EntitySummaryDetails from 'components/common/EntitySummaryDetails/EntitySummaryDetails';
import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder'; import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder';
import NextPrevious from 'components/common/next-previous/NextPrevious'; import NextPrevious from 'components/common/next-previous/NextPrevious';
import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichTextEditorPreviewer'; import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichTextEditorPreviewer';
import { TitleBreadcrumbProps } from 'components/common/title-breadcrumb/title-breadcrumb.interface';
import PageLayoutV1 from 'components/containers/PageLayoutV1'; import PageLayoutV1 from 'components/containers/PageLayoutV1';
import { EntityHeader } from 'components/Entity/EntityHeader/EntityHeader.component'; import { DataAssetsHeader } from 'components/DataAssets/DataAssetsHeader/DataAssetsHeader.component';
import Loader from 'components/Loader/Loader'; import Loader from 'components/Loader/Loader';
import { EntityName } from 'components/Modals/EntityNameModal/EntityNameModal.interface'; import { EntityName } from 'components/Modals/EntityNameModal/EntityNameModal.interface';
import { usePermissionProvider } from 'components/PermissionProvider/PermissionProvider'; import { usePermissionProvider } from 'components/PermissionProvider/PermissionProvider';
@ -45,15 +33,15 @@ import {
ResourceEntity, ResourceEntity,
} from 'components/PermissionProvider/PermissionProvider.interface'; } from 'components/PermissionProvider/PermissionProvider.interface';
import TabsLabel from 'components/TabsLabel/TabsLabel.component'; import TabsLabel from 'components/TabsLabel/TabsLabel.component';
import TagsContainer from 'components/Tag/TagsContainer/tags-container'; import TagsContainerV2 from 'components/Tag/TagsContainerV2/TagsContainerV2';
import { ERROR_PLACEHOLDER_TYPE } from 'enums/common.enum'; import { ERROR_PLACEHOLDER_TYPE } from 'enums/common.enum';
import { compare, Operation } from 'fast-json-patch'; import { compare, Operation } from 'fast-json-patch';
import { LabelType } from 'generated/entity/data/table'; import { LabelType } from 'generated/entity/data/table';
import { Include } from 'generated/type/include'; import { Include } from 'generated/type/include';
import { State } from 'generated/type/tagLabel'; import { State, TagSource } from 'generated/type/tagLabel';
import { isEmpty, isNil, isUndefined, startCase } from 'lodash'; import { isEmpty, isNil, isUndefined } from 'lodash';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { EntityTags, ExtraInfo, TagOption } from 'Models'; import { EntityTags } from 'Models';
import React, { import React, {
FunctionComponent, FunctionComponent,
useCallback, useCallback,
@ -70,23 +58,17 @@ import {
patchDatabaseDetails, patchDatabaseDetails,
} from 'rest/databaseAPI'; } from 'rest/databaseAPI';
import { getFeedCount, postThread } from 'rest/feedsAPI'; import { getFeedCount, postThread } from 'rest/feedsAPI';
import { fetchTagsAndGlossaryTerms } from 'utils/TagsUtils';
import { default as appState } from '../../AppState'; import { default as appState } from '../../AppState';
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
import { import {
getDatabaseDetailsPath, getDatabaseDetailsPath,
getDatabaseSchemaDetailsPath, getDatabaseSchemaDetailsPath,
getExplorePath, getExplorePath,
getServiceDetailsPath,
getTeamAndUserDetailsPath,
PAGE_SIZE, PAGE_SIZE,
pagingObject, pagingObject,
} from '../../constants/constants'; } from '../../constants/constants';
import { EntityField } from '../../constants/Feeds.constants'; import { EntityField } from '../../constants/Feeds.constants';
import { GlobalSettingsMenuCategory } from '../../constants/GlobalSettings.constants'; import { EntityTabs, EntityType } from '../../enums/entity.enum';
import { EntityInfo, EntityTabs, EntityType } from '../../enums/entity.enum';
import { ServiceCategory } from '../../enums/service.enum';
import { OwnerType } from '../../enums/user.enum';
import { CreateThread } from '../../generated/api/feed/createThread'; import { CreateThread } from '../../generated/api/feed/createThread';
import { Database } from '../../generated/entity/data/database'; import { Database } from '../../generated/entity/data/database';
import { DatabaseSchema } from '../../generated/entity/data/databaseSchema'; import { DatabaseSchema } from '../../generated/entity/data/databaseSchema';
@ -94,14 +76,13 @@ import { EntityReference } from '../../generated/entity/teams/user';
import { UsageDetails } from '../../generated/type/entityUsage'; import { UsageDetails } from '../../generated/type/entityUsage';
import { Paging } from '../../generated/type/paging'; import { Paging } from '../../generated/type/paging';
import { EntityFieldThreadCount } from '../../interface/feed.interface'; import { EntityFieldThreadCount } from '../../interface/feed.interface';
import { getEntityFeedLink, getEntityName } from '../../utils/EntityUtils'; import {
getEntityFeedLink,
getEntityName,
getEntityThreadLink,
} from '../../utils/EntityUtils';
import { getEntityFieldThreadCounts } from '../../utils/FeedUtils'; import { getEntityFieldThreadCounts } from '../../utils/FeedUtils';
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
import { getSettingPath } from '../../utils/RouterUtils';
import {
getServiceRouteFromServiceType,
serviceTypeLogo,
} from '../../utils/ServiceUtils';
import { getErrorText } from '../../utils/StringsUtils'; import { getErrorText } from '../../utils/StringsUtils';
import { import {
getTagsWithoutTier, getTagsWithoutTier,
@ -113,16 +94,13 @@ import { showErrorToast } from '../../utils/ToastUtils';
const DatabaseDetails: FunctionComponent = () => { const DatabaseDetails: FunctionComponent = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider(); const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider();
const [slashedDatabaseName, setSlashedDatabaseName] = useState<
TitleBreadcrumbProps['titleLinks']
>([]);
const { getEntityPermissionByFqn } = usePermissionProvider(); const { getEntityPermissionByFqn } = usePermissionProvider();
const { databaseFQN, tab: activeTab = EntityTabs.SCHEMA } = const { databaseFQN, tab: activeTab = EntityTabs.SCHEMA } =
useParams<{ databaseFQN: string; tab: EntityTabs }>(); useParams<{ databaseFQN: string; tab: EntityTabs }>();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [showDeletedSchemas, setShowDeletedSchemas] = useState<boolean>(false); const [showDeletedSchemas, setShowDeletedSchemas] = useState<boolean>(false);
const [database, setDatabase] = useState<Database>(); const [database, setDatabase] = useState<Database>({} as Database);
const [serviceType, setServiceType] = useState<string>(); const [serviceType, setServiceType] = useState<string>();
const [schemaData, setSchemaData] = useState<DatabaseSchema[]>([]); const [schemaData, setSchemaData] = useState<DatabaseSchema[]>([]);
const [schemaDataLoading, setSchemaDataLoading] = useState<boolean>(true); const [schemaDataLoading, setSchemaDataLoading] = useState<boolean>(true);
@ -148,9 +126,6 @@ const DatabaseDetails: FunctionComponent = () => {
const [threadLink, setThreadLink] = useState<string>(''); const [threadLink, setThreadLink] = useState<string>('');
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [isEditable, setIsEditable] = useState<boolean>(false);
const [tagList, setTagList] = useState<Array<TagOption>>([]);
const [isTagLoading, setIsTagLoading] = useState<boolean>(false);
const history = useHistory(); const history = useHistory();
const isMounting = useRef(true); const isMounting = useRef(true);
@ -158,8 +133,6 @@ const DatabaseDetails: FunctionComponent = () => {
const tier = getTierTags(database?.tags ?? []); const tier = getTierTags(database?.tags ?? []);
const tags = getTagsWithoutTier(database?.tags ?? []); const tags = getTagsWithoutTier(database?.tags ?? []);
const deleted = database?.deleted;
const [databasePermission, setDatabasePermission] = const [databasePermission, setDatabasePermission] =
useState<OperationPermission>(DEFAULT_ENTITY_PERMISSION); useState<OperationPermission>(DEFAULT_ENTITY_PERMISSION);
@ -178,57 +151,6 @@ const DatabaseDetails: FunctionComponent = () => {
} }
}; };
const tabs = useMemo(() => {
return [
{
label: (
<TabsLabel
count={databaseSchemaInstanceCount}
id={EntityTabs.SCHEMA}
isActive={activeTab === EntityTabs.SCHEMA}
name={t('label.schema-plural')}
/>
),
key: EntityTabs.SCHEMA,
},
{
label: (
<TabsLabel
count={feedCount}
id={EntityTabs.ACTIVITY_FEED}
isActive={activeTab === EntityTabs.ACTIVITY_FEED}
name={t('label.activity-feed-plural')}
/>
),
key: EntityTabs.ACTIVITY_FEED,
},
];
}, [activeTab, databaseSchemaInstanceCount, feedCount]);
const extraInfo: Array<ExtraInfo> = [
{
key: EntityInfo.OWNER,
value:
database?.owner?.type === 'team'
? getTeamAndUserDetailsPath(
database?.owner?.displayName || database?.owner?.name || ''
)
: database?.owner?.displayName || database?.owner?.name || '',
placeholderText:
database?.owner?.displayName || database?.owner?.name || '',
isLink: database?.owner?.type === 'team',
openInNewTab: false,
profileName:
database?.owner?.type === OwnerType.USER
? database?.owner?.name
: undefined,
},
{
key: EntityInfo.TIER,
value: tier?.tagFQN ? tier.tagFQN.split(FQN_SEPARATOR_CHAR)[1] : '',
},
];
const fetchDatabaseSchemas = (pagingObj?: string) => { const fetchDatabaseSchemas = (pagingObj?: string) => {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
setSchemaDataLoading(true); setSchemaDataLoading(true);
@ -302,34 +224,12 @@ const DatabaseDetails: FunctionComponent = () => {
getDatabaseDetailsByFQN(databaseFQN, ['owner', 'tags']) getDatabaseDetailsByFQN(databaseFQN, ['owner', 'tags'])
.then((res) => { .then((res) => {
if (res) { if (res) {
const { description, id, name, service, serviceType } = res; const { description, id, name, serviceType } = res;
setDatabase(res); setDatabase(res);
setDescription(description ?? ''); setDescription(description ?? '');
setDatabaseId(id ?? ''); setDatabaseId(id ?? '');
setDatabaseName(name); setDatabaseName(name);
setServiceType(serviceType); setServiceType(serviceType);
setSlashedDatabaseName([
{
name: startCase(ServiceCategory.DATABASE_SERVICES),
url: getSettingPath(
GlobalSettingsMenuCategory.SERVICES,
getServiceRouteFromServiceType(
ServiceCategory.DATABASE_SERVICES
)
),
},
{
name: getEntityName(service),
url: service.name
? getServiceDetailsPath(
service.name,
ServiceCategory.DATABASE_SERVICES
)
: '',
},
]);
fetchDatabaseSchemasAndDBTModels(); fetchDatabaseSchemasAndDBTModels();
} else { } else {
throw t('server.unexpected-response'); throw t('server.unexpected-response');
@ -431,13 +331,13 @@ const DatabaseDetails: FunctionComponent = () => {
}; };
const handleUpdateOwner = useCallback( const handleUpdateOwner = useCallback(
(owner: Database['owner']) => { async (owner: Database['owner']) => {
const updatedData = { const updatedData = {
...database, ...database,
owner: owner ? { ...database?.owner, ...owner } : undefined, owner: owner ? { ...database?.owner, ...owner } : undefined,
}; };
settingsUpdateHandler(updatedData as Database); await settingsUpdateHandler(updatedData as Database);
}, },
[database, database?.owner, settingsUpdateHandler] [database, database?.owner, settingsUpdateHandler]
); );
@ -579,51 +479,19 @@ const DatabaseDetails: FunctionComponent = () => {
return settingsUpdateHandler(updatedTableDetails); return settingsUpdateHandler(updatedTableDetails);
}; };
const fetchTags = async () => {
setIsTagLoading(true);
try {
const tags = await fetchTagsAndGlossaryTerms();
setTagList(tags);
} catch (error) {
setTagList([]);
} finally {
setIsTagLoading(false);
}
};
const isTagEditable =
databasePermission.EditTags || databasePermission.EditAll;
const selectedTags = useMemo(() => {
return tier?.tagFQN
? [
...tags.map((tag) => ({
...tag,
isRemovable: true,
})),
{ tagFQN: tier.tagFQN, isRemovable: false },
]
: [
...tags.map((tag) => ({
...tag,
isRemovable: true,
})),
] ?? [];
}, [tier, tags]);
/** /**
* Formulates updated tags and updates table entity data for API call * Formulates updated tags and updates table entity data for API call
* @param selectedTags * @param selectedTags
*/ */
const onTagUpdate = (selectedTags?: Array<EntityTags>) => { const onTagUpdate = async (selectedTags?: Array<EntityTags>) => {
if (selectedTags) { if (selectedTags) {
const updatedTags = [...(tier ? [tier] : []), ...selectedTags]; const updatedTags = [...(tier ? [tier] : []), ...selectedTags];
const updatedTable = { ...database, tags: updatedTags }; const updatedTable = { ...database, tags: updatedTags };
settingsUpdateHandler(updatedTable as Database); await settingsUpdateHandler(updatedTable as Database);
} }
}; };
const handleTagSelection = (selectedTags?: Array<EntityTags>) => { const handleTagSelection = async (selectedTags: EntityTags[]) => {
if (selectedTags) { if (selectedTags) {
const prevTags = const prevTags =
tags?.filter((tag) => tags?.filter((tag) =>
@ -643,9 +511,8 @@ const DatabaseDetails: FunctionComponent = () => {
source: tag.source, source: tag.source,
tagFQN: tag.tagFQN, tagFQN: tag.tagFQN,
})); }));
onTagUpdate([...prevTags, ...newTags]); await onTagUpdate([...prevTags, ...newTags]);
} }
setIsEditable(false);
}; };
const databaseTable = useMemo(() => { const databaseTable = useMemo(() => {
@ -659,10 +526,6 @@ const DatabaseDetails: FunctionComponent = () => {
columns={tableColumn} columns={tableColumn}
data-testid="database-databaseSchemas" data-testid="database-databaseSchemas"
dataSource={schemaData} dataSource={schemaData}
loading={{
spinning: schemaDataLoading,
indicator: <Loader size="small" />,
}}
pagination={false} pagination={false}
rowKey="id" rowKey="id"
size="small" size="small"
@ -693,11 +556,136 @@ const DatabaseDetails: FunctionComponent = () => {
databaseSchemaPagingHandler, databaseSchemaPagingHandler,
]); ]);
const tabs = useMemo(
() => [
{
label: (
<TabsLabel
count={databaseSchemaInstanceCount}
id={EntityTabs.SCHEMA}
isActive={activeTab === EntityTabs.SCHEMA}
name={t('label.schema-plural')}
/>
),
key: EntityTabs.SCHEMA,
children: (
<Row gutter={[0, 16]} wrap={false}>
<Col className="p-t-sm m-l-lg" flex="auto">
<div className="d-flex flex-col gap-4">
<DescriptionV1
description={description}
entityFieldThreads={getEntityFieldThreadCounts(
EntityField.DESCRIPTION,
entityFieldThreadCount
)}
entityFqn={databaseFQN}
entityName={databaseName}
entityType={EntityType.DATABASE}
hasEditAccess={
databasePermission.EditDescription ||
databasePermission.EditAll
}
isEdit={isEdit}
onCancel={onCancel}
onDescriptionEdit={onDescriptionEdit}
onDescriptionUpdate={onDescriptionUpdate}
onThreadLinkSelect={onThreadLinkSelect}
/>
<Row justify="end">
<Col>
<Switch
checked={showDeletedSchemas}
data-testid="show-deleted"
onClick={setShowDeletedSchemas}
/>
<Typography.Text className="m-l-xs">
{t('label.deleted')}
</Typography.Text>{' '}
</Col>
</Row>
{databaseTable}
</div>
</Col>
<Col
className="entity-tag-right-panel-container"
data-testid="entity-right-panel"
flex="320px">
<Space className="w-full" direction="vertical" size="large">
<TagsContainerV2
entityFqn={databaseFQN}
entityThreadLink={getEntityThreadLink(entityFieldThreadCount)}
entityType={EntityType.DATABASE}
permission={
databasePermission.EditDescription ||
(databasePermission.EditAll && !database?.deleted)
}
selectedTags={tags}
tagType={TagSource.Classification}
onSelectionChange={handleTagSelection}
onThreadLinkSelect={onThreadLinkSelect}
/>
<TagsContainerV2
entityFqn={databaseFQN}
entityThreadLink={getEntityThreadLink(entityFieldThreadCount)}
entityType={EntityType.DATABASE}
permission={
databasePermission.EditDescription ||
(databasePermission.EditAll && !database?.deleted)
}
selectedTags={tags}
tagType={TagSource.Glossary}
onSelectionChange={handleTagSelection}
onThreadLinkSelect={onThreadLinkSelect}
/>
</Space>
</Col>
</Row>
),
},
{
label: (
<TabsLabel
count={feedCount}
id={EntityTabs.ACTIVITY_FEED}
isActive={activeTab === EntityTabs.ACTIVITY_FEED}
name={t('label.activity-feed-plural')}
/>
),
key: EntityTabs.ACTIVITY_FEED,
children: (
<ActivityFeedProvider>
<ActivityFeedTab
entityType={EntityType.DATABASE}
fqn={database?.fullyQualifiedName ?? ''}
onFeedUpdate={getEntityFeedCount}
/>
</ActivityFeedProvider>
),
},
],
[
tags,
isEdit,
database,
description,
databaseName,
entityFieldThreadCount,
databaseFQN,
activeTab,
databaseTable,
databasePermission,
databaseSchemaInstanceCount,
feedCount,
showDeletedSchemas,
]
);
useEffect(() => { useEffect(() => {
fetchDatabaseSchemas(); fetchDatabaseSchemas();
}, [showDeletedSchemas]); }, [showDeletedSchemas]);
if (isLoading) { if (isLoading || isDatabaseDetailsLoading) {
return <Loader />; return <Loader />;
} }
@ -709,217 +697,53 @@ const DatabaseDetails: FunctionComponent = () => {
); );
} }
return ( if (!(databasePermission.ViewAll || databasePermission.ViewBasic)) {
<> return <ErrorPlaceHolder type={ERROR_PLACEHOLDER_TYPE.PERMISSION} />;
{databasePermission.ViewAll || databasePermission.ViewBasic ? ( }
<PageLayoutV1
pageTitle={t('label.entity-detail-plural', {
entity: getEntityName(database),
})}>
<Row className="page-container">
{isDatabaseDetailsLoading ? (
<Skeleton
active
paragraph={{
rows: 3,
width: ['20%', '80%', '60%'],
}}
/>
) : (
<Col span={24}>
{database && (
<Row wrap={false}>
<Col flex="auto">
<EntityHeader
breadcrumb={slashedDatabaseName}
entityData={database}
entityType={EntityType.DATABASE}
icon={
<img
className="h-8"
src={serviceTypeLogo(serviceType ?? '')}
/>
}
serviceName={database.service.name ?? ''}
/>
</Col>
<Col flex="30px">
<ManageButton
isRecursiveDelete
allowSoftDelete={false}
canDelete={databasePermission.Delete}
displayName={database.displayName}
editDisplayNamePermission={
databasePermission.EditAll ||
databasePermission.EditDisplayName
}
entityFQN={databaseFQN}
entityId={databaseId}
entityName={databaseName}
entityType={EntityType.DATABASE}
onEditDisplayName={handleUpdateDisplayName}
/>
</Col>
</Row>
)}
<Col className="m-t-xs" span={24}>
<Space wrap align="center" data-testid="extrainfo" size={4}>
{extraInfo.map((info, index) => (
<span
className="d-flex tw-items-center"
data-testid={info.key || `info${index}`}
key={index}>
<EntitySummaryDetails
currentOwner={database?.owner}
data={info}
tier={getTierTags(database?.tags ?? [])}
updateOwner={
databasePermission.EditOwner ||
databasePermission.EditAll
? handleUpdateOwner
: undefined
}
updateTier={
databasePermission.EditTags ||
databasePermission.EditAll
? handleUpdateTier
: undefined
}
/>
{extraInfo.length !== 1 &&
index < extraInfo.length - 1 ? (
<span className="tw-mx-1.5 tw-inline-block tw-text-gray-400">
{t('label.pipe-symbol')}
</span>
) : null}
</span>
))}
</Space>
</Col>
<Col className="m-t-xs" span={24}>
<Space
wrap
align="center"
data-testid="entity-tags"
size={6}
onClick={() => {
if (isTagEditable) {
// Fetch tags and terms only once
if (tagList.length === 0) {
fetchTags();
}
setIsEditable(true);
}
}}>
{!deleted && (
<TagsContainer
className="w-min-20"
dropDownHorzPosRight={false}
editable={isEditable}
isLoading={isTagLoading}
selectedTags={selectedTags}
showAddTagButton={
isTagEditable && isEmpty(selectedTags)
}
showEditTagButton={isTagEditable}
size="small"
tagList={tagList}
onCancel={() => {
handleTagSelection();
}}
onSelectionChange={(tags) => {
handleTagSelection(tags);
}}
/>
)}
</Space>
</Col>
</Col>
)}
<Col span={24}> return (
<Row className="m-t-md"> <PageLayoutV1
<Col span={24}> className="bg-white"
<Tabs pageTitle={t('label.entity-detail-plural', {
activeKey={activeTab ?? EntityTabs.SCHEMA} entity: getEntityName(database),
items={tabs} })}>
onChange={activeTabHandler} <Row gutter={[0, 12]}>
/> <Col className="p-x-lg" span={24}>
</Col> <DataAssetsHeader
<Col className="p-y-md" span={24}> isRecursiveDelete
{activeTab === EntityTabs.SCHEMA && ( allowSoftDelete={false}
<> dataAsset={database}
<Row gutter={[16, 16]}> entityType={EntityType.DATABASE}
<Col data-testid="description-container" span={24}> permissions={databasePermission}
<DescriptionV1 onDisplayNameUpdate={handleUpdateDisplayName}
description={description} onOwnerUpdate={handleUpdateOwner}
entityFieldThreads={getEntityFieldThreadCounts( onRestoreDataAsset={() => Promise.resolve()}
EntityField.DESCRIPTION, onTierUpdate={handleUpdateTier}
entityFieldThreadCount />
)} </Col>
entityFqn={databaseFQN} <Col span={24}>
entityName={databaseName} <Tabs
entityType={EntityType.DATABASE} activeKey={activeTab ?? EntityTabs.SCHEMA}
hasEditAccess={ className="entity-details-page-tabs"
databasePermission.EditDescription || data-testid="tabs"
databasePermission.EditAll items={tabs}
} onChange={activeTabHandler}
isEdit={isEdit} />
onCancel={onCancel} </Col>
onDescriptionEdit={onDescriptionEdit}
onDescriptionUpdate={onDescriptionUpdate} {threadLink ? (
onThreadLinkSelect={onThreadLinkSelect} <ActivityThreadPanel
/> createThread={createThread}
</Col> deletePostHandler={deleteFeed}
<Col span={24}> open={Boolean(threadLink)}
<Row justify="end"> postFeedHandler={postFeed}
<Col> threadLink={threadLink}
<Switch updateThreadHandler={updateFeed}
checked={showDeletedSchemas} onCancel={onThreadPanelClose}
data-testid="show-deleted" />
onClick={setShowDeletedSchemas} ) : null}
/> </Row>
<Typography.Text className="m-l-xs"> </PageLayoutV1>
{t('label.deleted')}
</Typography.Text>{' '}
</Col>
</Row>
</Col>
{databaseTable}
</Row>
</>
)}
{activeTab === EntityTabs.ACTIVITY_FEED && (
<ActivityFeedProvider>
<ActivityFeedTab
entityType={EntityType.DATABASE}
fqn={database?.fullyQualifiedName ?? ''}
onFeedUpdate={getEntityFeedCount}
/>
</ActivityFeedProvider>
)}
</Col>
</Row>
</Col>
<Col span={24}>
{threadLink ? (
<ActivityThreadPanel
createThread={createThread}
deletePostHandler={deleteFeed}
open={Boolean(threadLink)}
postFeedHandler={postFeed}
threadLink={threadLink}
updateThreadHandler={updateFeed}
onCancel={onThreadPanelClose}
/>
) : null}
</Col>
</Row>
</PageLayoutV1>
) : (
<ErrorPlaceHolder type={ERROR_PLACEHOLDER_TYPE.PERMISSION} />
)}
</>
); );
}; };

View File

@ -28,10 +28,12 @@ import {
SourceType, SourceType,
} from 'components/searched-data/SearchedData.interface'; } from 'components/searched-data/SearchedData.interface';
import { EntityField } from 'constants/Feeds.constants'; import { EntityField } from 'constants/Feeds.constants';
import { GlobalSettingsMenuCategory } from 'constants/GlobalSettings.constants';
import { ExplorePageTabs } from 'enums/Explore.enum'; import { ExplorePageTabs } from 'enums/Explore.enum';
import { Tag } from 'generated/entity/classification/tag'; import { Tag } from 'generated/entity/classification/tag';
import { Container } from 'generated/entity/data/container'; import { Container } from 'generated/entity/data/container';
import { DashboardDataModel } from 'generated/entity/data/dashboardDataModel'; import { DashboardDataModel } from 'generated/entity/data/dashboardDataModel';
import { Database } from 'generated/entity/data/database';
import { GlossaryTerm } from 'generated/entity/data/glossaryTerm'; import { GlossaryTerm } from 'generated/entity/data/glossaryTerm';
import { Mlmodel } from 'generated/entity/data/mlmodel'; import { Mlmodel } from 'generated/entity/data/mlmodel';
import { Topic } from 'generated/entity/data/topic'; import { Topic } from 'generated/entity/data/topic';
@ -79,7 +81,8 @@ import {
} from './CommonUtils'; } from './CommonUtils';
import { getEntityFieldThreadCounts } from './FeedUtils'; import { getEntityFieldThreadCounts } from './FeedUtils';
import Fqn from './Fqn'; import Fqn from './Fqn';
import { getGlossaryPath } from './RouterUtils'; import { getGlossaryPath, getSettingPath } from './RouterUtils';
import { getServiceRouteFromServiceType } from './ServiceUtils';
import { import {
getDataTypeString, getDataTypeString,
getTierFromTableTags, getTierFromTableTags,
@ -985,7 +988,10 @@ export const getBreadcrumbForEntitiesWithServiceOnly = (
}; };
export const getEntityBreadcrumbs = ( export const getEntityBreadcrumbs = (
entity: SearchedDataProps['data'][number]['_source'] | DashboardDataModel, entity:
| SearchedDataProps['data'][number]['_source']
| DashboardDataModel
| Database,
entityType?: EntityType, entityType?: EntityType,
includeCurrent = false includeCurrent = false
) => { ) => {
@ -1029,6 +1035,18 @@ export const getEntityBreadcrumbs = (
})), })),
]; ];
case EntityType.DATABASE:
return [
{
name: startCase(ServiceCategory.DATABASE_SERVICES),
url: getSettingPath(
GlobalSettingsMenuCategory.SERVICES,
getServiceRouteFromServiceType(ServiceCategory.DATABASE_SERVICES)
),
},
...getBreadcrumbForEntitiesWithServiceOnly(entity as Database),
];
case EntityType.TOPIC: case EntityType.TOPIC:
case EntityType.DASHBOARD: case EntityType.DASHBOARD:
case EntityType.PIPELINE: case EntityType.PIPELINE: