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

View File

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

View File

@ -128,7 +128,8 @@ export type EntityWithServices =
| Pipeline
| Mlmodel
| Container
| DashboardDataModel;
| DashboardDataModel
| Database;
export interface EntityDetailsObjectInterface {
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 = {
data: [
{
@ -100,59 +90,6 @@ const mockSchemaData = {
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 = {
totalCount: 6,
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', () => {
return jest.fn().mockReturnValue({
inPageSearchText: '',
@ -229,60 +179,24 @@ jest.mock('rest/databaseAPI', () => ({
}));
jest.mock('rest/feedsAPI', () => ({
getAllFeeds: jest
.fn()
.mockImplementation(() => Promise.resolve(mockAllFeeds)),
getFeedCount: jest
.fn()
.mockImplementation(() => Promise.resolve(mockFeedCount)),
postFeedById: 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', () => ({
getOwnerFromId: jest.fn().mockReturnValue({
name: 'owner',
id: 'string',
type: 'user',
}),
getUsagePercentile: jest.fn().mockReturnValue('Medium - 45th pctile'),
getTierTags: 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', () => {
return jest.fn().mockReturnValue(<div>NextPrevious</div>);
});
jest.mock(
'components/common/title-breadcrumb/title-breadcrumb.component',
() => {
return jest.fn().mockReturnValue(<div>TitleBreadcrumb</div>);
}
);
jest.mock('components/FeedEditor/FeedEditor', () => {
return jest.fn().mockReturnValue(<p>FeedEditor</p>);
jest.mock('components/Tag/TagsContainerV2/TagsContainerV2', () => {
return jest.fn().mockReturnValue(<div>TagsContainerV2</div>);
});
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>);
});
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', () => {
return jest.fn().mockImplementation(({ children }) => children);
});
jest.mock('components/Entity/EntityHeader/EntityHeader.component', () => ({
EntityHeader: jest.fn().mockImplementation(() => <p>EntityHeader</p>),
}));
jest.mock(
'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', () => {
it('Component should render', async () => {
@ -347,11 +260,8 @@ describe('Test DatabaseDetails page', () => {
wrapper: MemoryRouter,
});
const entityHeader = await findByText(container, 'EntityHeader');
const descriptionContainer = await findByTestId(
container,
'description-container'
);
const entityHeader = await findByText(container, 'DataAssetsHeader');
const descriptionContainer = await findByText(container, 'Description');
const databaseTable = await findByTestId(
container,
'database-databaseSchemas'
@ -421,11 +331,8 @@ describe('Test DatabaseDetails page', () => {
wrapper: MemoryRouter,
});
const entityHeader = await findByText(container, 'EntityHeader');
const descriptionContainer = await findByTestId(
container,
'description-container'
);
const entityHeader = await findByText(container, 'DataAssetsHeader');
const descriptionContainer = await findByText(container, 'Description');
const databaseTable = await findByTestId(
container,
'database-databaseSchemas'

View File

@ -11,16 +11,7 @@
* limitations under the License.
*/
import {
Col,
Row,
Skeleton,
Space,
Switch,
Table,
Tabs,
Typography,
} from 'antd';
import { Col, Row, Space, Switch, Table, Tabs, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { AxiosError } from 'axios';
import ActivityFeedProvider, {
@ -29,14 +20,11 @@ import ActivityFeedProvider, {
import { ActivityFeedTab } from 'components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component';
import ActivityThreadPanel from 'components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel';
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 NextPrevious from 'components/common/next-previous/NextPrevious';
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 { EntityHeader } from 'components/Entity/EntityHeader/EntityHeader.component';
import { DataAssetsHeader } from 'components/DataAssets/DataAssetsHeader/DataAssetsHeader.component';
import Loader from 'components/Loader/Loader';
import { EntityName } from 'components/Modals/EntityNameModal/EntityNameModal.interface';
import { usePermissionProvider } from 'components/PermissionProvider/PermissionProvider';
@ -45,15 +33,15 @@ import {
ResourceEntity,
} from 'components/PermissionProvider/PermissionProvider.interface';
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 { compare, Operation } from 'fast-json-patch';
import { LabelType } from 'generated/entity/data/table';
import { Include } from 'generated/type/include';
import { State } from 'generated/type/tagLabel';
import { isEmpty, isNil, isUndefined, startCase } from 'lodash';
import { State, TagSource } from 'generated/type/tagLabel';
import { isEmpty, isNil, isUndefined } from 'lodash';
import { observer } from 'mobx-react';
import { EntityTags, ExtraInfo, TagOption } from 'Models';
import { EntityTags } from 'Models';
import React, {
FunctionComponent,
useCallback,
@ -70,23 +58,17 @@ import {
patchDatabaseDetails,
} from 'rest/databaseAPI';
import { getFeedCount, postThread } from 'rest/feedsAPI';
import { fetchTagsAndGlossaryTerms } from 'utils/TagsUtils';
import { default as appState } from '../../AppState';
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
import {
getDatabaseDetailsPath,
getDatabaseSchemaDetailsPath,
getExplorePath,
getServiceDetailsPath,
getTeamAndUserDetailsPath,
PAGE_SIZE,
pagingObject,
} from '../../constants/constants';
import { EntityField } from '../../constants/Feeds.constants';
import { GlobalSettingsMenuCategory } from '../../constants/GlobalSettings.constants';
import { EntityInfo, EntityTabs, EntityType } from '../../enums/entity.enum';
import { ServiceCategory } from '../../enums/service.enum';
import { OwnerType } from '../../enums/user.enum';
import { EntityTabs, EntityType } from '../../enums/entity.enum';
import { CreateThread } from '../../generated/api/feed/createThread';
import { Database } from '../../generated/entity/data/database';
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 { Paging } from '../../generated/type/paging';
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 { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
import { getSettingPath } from '../../utils/RouterUtils';
import {
getServiceRouteFromServiceType,
serviceTypeLogo,
} from '../../utils/ServiceUtils';
import { getErrorText } from '../../utils/StringsUtils';
import {
getTagsWithoutTier,
@ -113,16 +94,13 @@ import { showErrorToast } from '../../utils/ToastUtils';
const DatabaseDetails: FunctionComponent = () => {
const { t } = useTranslation();
const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider();
const [slashedDatabaseName, setSlashedDatabaseName] = useState<
TitleBreadcrumbProps['titleLinks']
>([]);
const { getEntityPermissionByFqn } = usePermissionProvider();
const { databaseFQN, tab: activeTab = EntityTabs.SCHEMA } =
useParams<{ databaseFQN: string; tab: EntityTabs }>();
const [isLoading, setIsLoading] = useState(true);
const [showDeletedSchemas, setShowDeletedSchemas] = useState<boolean>(false);
const [database, setDatabase] = useState<Database>();
const [database, setDatabase] = useState<Database>({} as Database);
const [serviceType, setServiceType] = useState<string>();
const [schemaData, setSchemaData] = useState<DatabaseSchema[]>([]);
const [schemaDataLoading, setSchemaDataLoading] = useState<boolean>(true);
@ -148,9 +126,6 @@ const DatabaseDetails: FunctionComponent = () => {
const [threadLink, setThreadLink] = useState<string>('');
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 isMounting = useRef(true);
@ -158,8 +133,6 @@ const DatabaseDetails: FunctionComponent = () => {
const tier = getTierTags(database?.tags ?? []);
const tags = getTagsWithoutTier(database?.tags ?? []);
const deleted = database?.deleted;
const [databasePermission, setDatabasePermission] =
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) => {
return new Promise<void>((resolve, reject) => {
setSchemaDataLoading(true);
@ -302,34 +224,12 @@ const DatabaseDetails: FunctionComponent = () => {
getDatabaseDetailsByFQN(databaseFQN, ['owner', 'tags'])
.then((res) => {
if (res) {
const { description, id, name, service, serviceType } = res;
const { description, id, name, serviceType } = res;
setDatabase(res);
setDescription(description ?? '');
setDatabaseId(id ?? '');
setDatabaseName(name);
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();
} else {
throw t('server.unexpected-response');
@ -431,13 +331,13 @@ const DatabaseDetails: FunctionComponent = () => {
};
const handleUpdateOwner = useCallback(
(owner: Database['owner']) => {
async (owner: Database['owner']) => {
const updatedData = {
...database,
owner: owner ? { ...database?.owner, ...owner } : undefined,
};
settingsUpdateHandler(updatedData as Database);
await settingsUpdateHandler(updatedData as Database);
},
[database, database?.owner, settingsUpdateHandler]
);
@ -579,51 +479,19 @@ const DatabaseDetails: FunctionComponent = () => {
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
* @param selectedTags
*/
const onTagUpdate = (selectedTags?: Array<EntityTags>) => {
const onTagUpdate = async (selectedTags?: Array<EntityTags>) => {
if (selectedTags) {
const updatedTags = [...(tier ? [tier] : []), ...selectedTags];
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) {
const prevTags =
tags?.filter((tag) =>
@ -643,9 +511,8 @@ const DatabaseDetails: FunctionComponent = () => {
source: tag.source,
tagFQN: tag.tagFQN,
}));
onTagUpdate([...prevTags, ...newTags]);
await onTagUpdate([...prevTags, ...newTags]);
}
setIsEditable(false);
};
const databaseTable = useMemo(() => {
@ -659,10 +526,6 @@ const DatabaseDetails: FunctionComponent = () => {
columns={tableColumn}
data-testid="database-databaseSchemas"
dataSource={schemaData}
loading={{
spinning: schemaDataLoading,
indicator: <Loader size="small" />,
}}
pagination={false}
rowKey="id"
size="small"
@ -693,11 +556,136 @@ const DatabaseDetails: FunctionComponent = () => {
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(() => {
fetchDatabaseSchemas();
}, [showDeletedSchemas]);
if (isLoading) {
if (isLoading || isDatabaseDetailsLoading) {
return <Loader />;
}
@ -709,217 +697,53 @@ const DatabaseDetails: FunctionComponent = () => {
);
}
return (
<>
{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>
)}
if (!(databasePermission.ViewAll || databasePermission.ViewBasic)) {
return <ErrorPlaceHolder type={ERROR_PLACEHOLDER_TYPE.PERMISSION} />;
}
<Col span={24}>
<Row className="m-t-md">
<Col span={24}>
<Tabs
activeKey={activeTab ?? EntityTabs.SCHEMA}
items={tabs}
onChange={activeTabHandler}
/>
</Col>
<Col className="p-y-md" span={24}>
{activeTab === EntityTabs.SCHEMA && (
<>
<Row gutter={[16, 16]}>
<Col data-testid="description-container" span={24}>
<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}
/>
</Col>
<Col span={24}>
<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>
</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} />
)}
</>
return (
<PageLayoutV1
className="bg-white"
pageTitle={t('label.entity-detail-plural', {
entity: getEntityName(database),
})}>
<Row gutter={[0, 12]}>
<Col className="p-x-lg" span={24}>
<DataAssetsHeader
isRecursiveDelete
allowSoftDelete={false}
dataAsset={database}
entityType={EntityType.DATABASE}
permissions={databasePermission}
onDisplayNameUpdate={handleUpdateDisplayName}
onOwnerUpdate={handleUpdateOwner}
onRestoreDataAsset={() => Promise.resolve()}
onTierUpdate={handleUpdateTier}
/>
</Col>
<Col span={24}>
<Tabs
activeKey={activeTab ?? EntityTabs.SCHEMA}
className="entity-details-page-tabs"
data-testid="tabs"
items={tabs}
onChange={activeTabHandler}
/>
</Col>
{threadLink ? (
<ActivityThreadPanel
createThread={createThread}
deletePostHandler={deleteFeed}
open={Boolean(threadLink)}
postFeedHandler={postFeed}
threadLink={threadLink}
updateThreadHandler={updateFeed}
onCancel={onThreadPanelClose}
/>
) : null}
</Row>
</PageLayoutV1>
);
};

View File

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