mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2026-01-06 12:36:56 +00:00
fix(ui): breadcrumb for details page (#11084)
* fix(ui): breadcrumb for details page support tag, tier for database details page * fix unit tests
This commit is contained in:
parent
072dfd199c
commit
af853d7b4f
@ -159,7 +159,7 @@ const TagsContainer: FunctionComponent<TagsContainerProps> = ({
|
||||
size="small"
|
||||
type="text">
|
||||
<IconEdit
|
||||
className="anticon"
|
||||
className="anticon align-middle"
|
||||
height={16}
|
||||
name={t('label.edit')}
|
||||
width={16}
|
||||
|
||||
@ -31,7 +31,6 @@ import {
|
||||
removeFollower,
|
||||
} from 'rest/dashboardAPI';
|
||||
import { getAllFeeds, postFeedById, postThread } from 'rest/feedsAPI';
|
||||
import { serviceTypeLogo } from 'utils/ServiceUtils';
|
||||
import AppState from '../../AppState';
|
||||
import {
|
||||
getDashboardDetailsPath,
|
||||
@ -215,12 +214,6 @@ const DashboardDetailsPage = () => {
|
||||
ServiceCategory.DASHBOARD_SERVICES
|
||||
)
|
||||
: '',
|
||||
imgSrc: serviceType ? serviceTypeLogo(serviceType) : undefined,
|
||||
},
|
||||
{
|
||||
name: getEntityName(res),
|
||||
url: '',
|
||||
activeTitle: true,
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@ -40,7 +40,6 @@ import {
|
||||
patchTableDetails,
|
||||
removeFollower,
|
||||
} from 'rest/tableAPI';
|
||||
import { serviceTypeLogo } from 'utils/ServiceUtils';
|
||||
import AppState from '../../AppState';
|
||||
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
|
||||
import {
|
||||
@ -230,7 +229,6 @@ const DatasetDetailsPage: FunctionComponent = () => {
|
||||
ServiceCategory.DATABASE_SERVICES
|
||||
)
|
||||
: '',
|
||||
imgSrc: serviceType ? serviceTypeLogo(serviceType) : undefined,
|
||||
},
|
||||
{
|
||||
name: getPartialNameFromTableFQN(databaseFullyQualifiedName, [
|
||||
@ -244,11 +242,6 @@ const DatasetDetailsPage: FunctionComponent = () => {
|
||||
]),
|
||||
url: getDatabaseSchemaDetailsPath(databaseSchemaFullyQualifiedName),
|
||||
},
|
||||
{
|
||||
name: getEntityName(res),
|
||||
url: '',
|
||||
activeTitle: true,
|
||||
},
|
||||
]);
|
||||
|
||||
addToRecentViewed({
|
||||
|
||||
@ -30,7 +30,6 @@ import {
|
||||
patchPipelineDetails,
|
||||
removeFollower,
|
||||
} from 'rest/pipelineAPI';
|
||||
import { serviceTypeLogo } from 'utils/ServiceUtils';
|
||||
import {
|
||||
getServiceDetailsPath,
|
||||
getVersionPath,
|
||||
@ -138,12 +137,6 @@ const PipelineDetailsPage = () => {
|
||||
ServiceCategory.PIPELINE_SERVICES
|
||||
)
|
||||
: '',
|
||||
imgSrc: serviceType ? serviceTypeLogo(serviceType) : undefined,
|
||||
},
|
||||
{
|
||||
name: getEntityName(res),
|
||||
url: '',
|
||||
activeTitle: true,
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@ -34,7 +34,6 @@ import {
|
||||
patchTopicDetails,
|
||||
removeFollower,
|
||||
} from 'rest/topicsAPI';
|
||||
import { serviceTypeLogo } from 'utils/ServiceUtils';
|
||||
import AppState from '../../AppState';
|
||||
import {
|
||||
getServiceDetailsPath,
|
||||
@ -208,12 +207,6 @@ const TopicDetailsPage: FunctionComponent = () => {
|
||||
ServiceCategory.MESSAGING_SERVICES
|
||||
)
|
||||
: '',
|
||||
imgSrc: serviceType ? serviceTypeLogo(serviceType) : undefined,
|
||||
},
|
||||
{
|
||||
name: getEntityName(res),
|
||||
url: '',
|
||||
activeTitle: true,
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@ -262,6 +262,8 @@ jest.mock('../../utils/TableUtils', () => ({
|
||||
type: 'user',
|
||||
}),
|
||||
getUsagePercentile: jest.fn().mockReturnValue('Medium - 45th pctile'),
|
||||
getTierTags: jest.fn().mockImplementation(() => ({})),
|
||||
getTagsWithoutTier: jest.fn().mockImplementation(() => []),
|
||||
}));
|
||||
|
||||
jest.mock('../../utils/CommonUtils', () => ({
|
||||
|
||||
@ -33,12 +33,14 @@ import {
|
||||
OperationPermission,
|
||||
ResourceEntity,
|
||||
} from 'components/PermissionProvider/PermissionProvider.interface';
|
||||
import TagsContainer from 'components/Tag/TagsContainer/tags-container';
|
||||
import { compare, Operation } from 'fast-json-patch';
|
||||
import { LabelType } from 'generated/entity/data/table';
|
||||
import { State } from 'generated/type/tagLabel';
|
||||
import { isNil, startCase } from 'lodash';
|
||||
import { observer } from 'mobx-react';
|
||||
import { ExtraInfo } from 'Models';
|
||||
import { EntityTags, ExtraInfo, TagOption } from 'Models';
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
RefObject,
|
||||
useCallback,
|
||||
@ -60,6 +62,7 @@ import {
|
||||
postFeedById,
|
||||
postThread,
|
||||
} from 'rest/feedsAPI';
|
||||
import { fetchTagsAndGlossaryTerms } from 'utils/TagsUtils';
|
||||
import { default as AppState, default as appState } from '../../AppState';
|
||||
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
|
||||
import {
|
||||
@ -74,7 +77,11 @@ import {
|
||||
import { EntityField } from '../../constants/Feeds.constants';
|
||||
import { GlobalSettingsMenuCategory } from '../../constants/GlobalSettings.constants';
|
||||
import { observerOptions } from '../../constants/Mydata.constants';
|
||||
import { EntityType, TabSpecificField } from '../../enums/entity.enum';
|
||||
import {
|
||||
EntityInfo,
|
||||
EntityType,
|
||||
TabSpecificField,
|
||||
} from '../../enums/entity.enum';
|
||||
import { ServiceCategory } from '../../enums/service.enum';
|
||||
import { OwnerType } from '../../enums/user.enum';
|
||||
import { CreateThread } from '../../generated/api/feed/createThread';
|
||||
@ -104,7 +111,11 @@ import {
|
||||
serviceTypeLogo,
|
||||
} from '../../utils/ServiceUtils';
|
||||
import { getErrorText } from '../../utils/StringsUtils';
|
||||
import { getUsagePercentile } from '../../utils/TableUtils';
|
||||
import {
|
||||
getTagsWithoutTier,
|
||||
getTierTags,
|
||||
getUsagePercentile,
|
||||
} from '../../utils/TableUtils';
|
||||
import { showErrorToast } from '../../utils/ToastUtils';
|
||||
|
||||
const DatabaseDetails: FunctionComponent = () => {
|
||||
@ -151,10 +162,18 @@ const DatabaseDetails: FunctionComponent = () => {
|
||||
const [paging, setPaging] = useState<Paging>({} as Paging);
|
||||
const [elementRef, isInView] = useInfiniteScroll(observerOptions);
|
||||
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);
|
||||
|
||||
const tier = getTierTags(database?.tags ?? []);
|
||||
const tags = getTagsWithoutTier(database?.tags ?? []);
|
||||
|
||||
const deleted = database?.deleted;
|
||||
|
||||
const [databasePermission, setDatabasePermission] =
|
||||
useState<OperationPermission>(DEFAULT_ENTITY_PERMISSION);
|
||||
|
||||
@ -202,7 +221,7 @@ const DatabaseDetails: FunctionComponent = () => {
|
||||
|
||||
const extraInfo: Array<ExtraInfo> = [
|
||||
{
|
||||
key: 'Owner',
|
||||
key: EntityInfo.OWNER,
|
||||
value:
|
||||
database?.owner?.type === 'team'
|
||||
? getTeamAndUserDetailsPath(
|
||||
@ -218,6 +237,10 @@ const DatabaseDetails: FunctionComponent = () => {
|
||||
? database?.owner?.name
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
key: EntityInfo.TIER,
|
||||
value: tier?.tagFQN ? tier.tagFQN.split(FQN_SEPARATOR_CHAR)[1] : '',
|
||||
},
|
||||
];
|
||||
|
||||
const fetchDatabaseSchemas = (pagingObj?: string) => {
|
||||
@ -286,7 +309,7 @@ const DatabaseDetails: FunctionComponent = () => {
|
||||
|
||||
const getDetailsByFQN = () => {
|
||||
setIsDatabaseDetailsLoading(true);
|
||||
getDatabaseDetailsByFQN(databaseFQN, ['owner'])
|
||||
getDatabaseDetailsByFQN(databaseFQN, ['owner', 'tags'])
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
const { description, id, name, service, serviceType } = res;
|
||||
@ -405,6 +428,19 @@ const DatabaseDetails: FunctionComponent = () => {
|
||||
setCurrentPage(activePage ?? 1);
|
||||
};
|
||||
|
||||
const settingsUpdateHandler = async (data: Database) => {
|
||||
try {
|
||||
const res = await saveUpdatedDatabaseData(data);
|
||||
|
||||
setDatabase(res);
|
||||
} catch (error) {
|
||||
showErrorToast(
|
||||
error,
|
||||
jsonData['api-error-messages']['update-database-error']
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateOwner = useCallback(
|
||||
(owner: Database['owner']) => {
|
||||
const updatedData = {
|
||||
@ -412,30 +448,9 @@ const DatabaseDetails: FunctionComponent = () => {
|
||||
owner: owner ? { ...database?.owner, ...owner } : undefined,
|
||||
};
|
||||
|
||||
return new Promise<void>((_, reject) => {
|
||||
saveUpdatedDatabaseData(updatedData as Database)
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
setDatabase(res);
|
||||
reject();
|
||||
} else {
|
||||
reject();
|
||||
|
||||
throw jsonData['api-error-messages'][
|
||||
'unexpected-server-response'
|
||||
];
|
||||
}
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
showErrorToast(
|
||||
err,
|
||||
jsonData['api-error-messages']['update-database-error']
|
||||
);
|
||||
reject();
|
||||
});
|
||||
});
|
||||
settingsUpdateHandler(updatedData as Database);
|
||||
},
|
||||
[database, database?.owner]
|
||||
[database, database?.owner, settingsUpdateHandler]
|
||||
);
|
||||
|
||||
const fetchActivityFeed = (after?: string) => {
|
||||
@ -641,6 +656,103 @@ const DatabaseDetails: FunctionComponent = () => {
|
||||
[]
|
||||
);
|
||||
|
||||
const handleUpdateTier = useCallback(
|
||||
(newTier?: string) => {
|
||||
if (newTier) {
|
||||
const tierTag = newTier
|
||||
? [
|
||||
...getTagsWithoutTier(database?.tags ?? []),
|
||||
{
|
||||
tagFQN: newTier,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
]
|
||||
: database?.tags;
|
||||
const updatedTableDetails = {
|
||||
...database,
|
||||
tags: tierTag,
|
||||
};
|
||||
|
||||
return settingsUpdateHandler(updatedTableDetails as Database);
|
||||
}
|
||||
|
||||
return;
|
||||
},
|
||||
[settingsUpdateHandler, database, tier]
|
||||
);
|
||||
|
||||
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 getSelectedTags = () => {
|
||||
return tier?.tagFQN
|
||||
? [
|
||||
...tags.map((tag) => ({
|
||||
...tag,
|
||||
isRemovable: true,
|
||||
})),
|
||||
{ tagFQN: tier.tagFQN, isRemovable: false },
|
||||
]
|
||||
: [
|
||||
...tags.map((tag) => ({
|
||||
...tag,
|
||||
isRemovable: true,
|
||||
})),
|
||||
] ?? [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Formulates updated tags and updates table entity data for API call
|
||||
* @param selectedTags
|
||||
*/
|
||||
const onTagUpdate = (selectedTags?: Array<EntityTags>) => {
|
||||
if (selectedTags) {
|
||||
const updatedTags = [...(tier ? [tier] : []), ...selectedTags];
|
||||
const updatedTable = { ...database, tags: updatedTags };
|
||||
settingsUpdateHandler(updatedTable as Database);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTagSelection = (selectedTags?: Array<EntityTags>) => {
|
||||
if (selectedTags) {
|
||||
const prevTags =
|
||||
tags?.filter((tag) =>
|
||||
selectedTags
|
||||
.map((selTag) => selTag.tagFQN)
|
||||
.includes(tag?.tagFQN as string)
|
||||
) || [];
|
||||
const newTags = selectedTags
|
||||
.filter((tag) => {
|
||||
return !prevTags
|
||||
?.map((prevTag) => prevTag.tagFQN)
|
||||
.includes(tag.tagFQN);
|
||||
})
|
||||
.map((tag) => ({
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
source: tag.source,
|
||||
tagFQN: tag.tagFQN,
|
||||
}));
|
||||
onTagUpdate([...prevTags, ...newTags]);
|
||||
}
|
||||
setIsEditable(false);
|
||||
};
|
||||
|
||||
console.log({ isEditable, isTagEditable, deleted });
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
@ -667,70 +779,107 @@ const DatabaseDetails: FunctionComponent = () => {
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{database && (
|
||||
<EntityHeader
|
||||
breadcrumb={slashedDatabaseName}
|
||||
entityData={database}
|
||||
entityType={EntityType.DATABASE}
|
||||
extra={
|
||||
<ManageButton
|
||||
isRecursiveDelete
|
||||
allowSoftDelete={false}
|
||||
canDelete={databasePermission.Delete}
|
||||
entityFQN={databaseFQN}
|
||||
entityId={databaseId}
|
||||
entityName={databaseName}
|
||||
entityType={EntityType.DATABASE}
|
||||
/>
|
||||
}
|
||||
icon={
|
||||
<img
|
||||
className="h-8"
|
||||
src={serviceTypeLogo(serviceType ?? '')}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Col span={24}>
|
||||
{extraInfo.map((info, index) => (
|
||||
<Space key={index}>
|
||||
<EntitySummaryDetails
|
||||
currentOwner={database?.owner}
|
||||
data={info}
|
||||
updateOwner={
|
||||
databasePermission.EditOwner ||
|
||||
databasePermission.EditAll
|
||||
? handleUpdateOwner
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Space>
|
||||
))}
|
||||
{database && (
|
||||
<EntityHeader
|
||||
breadcrumb={slashedDatabaseName}
|
||||
entityData={database}
|
||||
entityType={EntityType.DATABASE}
|
||||
extra={
|
||||
<ManageButton
|
||||
isRecursiveDelete
|
||||
allowSoftDelete={false}
|
||||
canDelete={databasePermission.Delete}
|
||||
entityFQN={databaseFQN}
|
||||
entityId={databaseId}
|
||||
entityName={databaseName}
|
||||
entityType={EntityType.DATABASE}
|
||||
/>
|
||||
}
|
||||
icon={
|
||||
<img
|
||||
className="h-8"
|
||||
src={serviceTypeLogo(serviceType ?? '')}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
<Col data-testid="description-container" span={24}>
|
||||
<Description
|
||||
description={description}
|
||||
entityFieldThreads={getEntityFieldThreadCounts(
|
||||
EntityField.DESCRIPTION,
|
||||
entityFieldThreadCount
|
||||
|
||||
<Col className="m-t-xs" span={24}>
|
||||
<Space
|
||||
wrap
|
||||
align="center"
|
||||
data-testid="extrainfo"
|
||||
size={4}>
|
||||
{extraInfo.map((info, index) => (
|
||||
<span
|
||||
className="tw-flex tw-items-center"
|
||||
data-testid={info.key || `info${index}`}
|
||||
key={index}>
|
||||
<EntitySummaryDetails
|
||||
currentOwner={database?.owner}
|
||||
data={info}
|
||||
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={() => {
|
||||
// Fetch tags and terms only once
|
||||
if (tagList.length === 0) {
|
||||
fetchTags();
|
||||
}
|
||||
setIsEditable(true);
|
||||
}}>
|
||||
{isTagEditable && !deleted && (
|
||||
<TagsContainer
|
||||
showEditTagButton
|
||||
className="w-min-20"
|
||||
dropDownHorzPosRight={false}
|
||||
editable={isEditable}
|
||||
isLoading={isTagLoading}
|
||||
selectedTags={getSelectedTags()}
|
||||
showAddTagButton={getSelectedTags().length === 0}
|
||||
size="small"
|
||||
tagList={tagList}
|
||||
onCancel={() => {
|
||||
handleTagSelection();
|
||||
}}
|
||||
onSelectionChange={(tags) => {
|
||||
handleTagSelection(tags);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
entityFqn={databaseFQN}
|
||||
entityName={databaseName}
|
||||
entityType={EntityType.DATABASE}
|
||||
hasEditAccess={
|
||||
databasePermission.EditDescription ||
|
||||
databasePermission.EditAll
|
||||
}
|
||||
isEdit={isEdit}
|
||||
onCancel={onCancel}
|
||||
onDescriptionEdit={onDescriptionEdit}
|
||||
onDescriptionUpdate={onDescriptionUpdate}
|
||||
onThreadLinkSelect={onThreadLinkSelect}
|
||||
/>
|
||||
</Space>
|
||||
</Col>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Col span={24}>
|
||||
<Row className="m-t-md">
|
||||
<Col span={24}>
|
||||
@ -743,34 +892,59 @@ const DatabaseDetails: FunctionComponent = () => {
|
||||
</Col>
|
||||
<Col className="p-y-md" span={24}>
|
||||
{activeTab === 1 && (
|
||||
<Fragment>
|
||||
<Table
|
||||
bordered
|
||||
className="table-shadow"
|
||||
columns={tableColumn}
|
||||
data-testid="database-databaseSchemas"
|
||||
dataSource={schemaData}
|
||||
loading={{
|
||||
spinning: schemaDataLoading,
|
||||
indicator: <Loader size="small" />,
|
||||
}}
|
||||
pagination={false}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
/>
|
||||
{Boolean(
|
||||
!isNil(databaseSchemaPaging.after) ||
|
||||
!isNil(databaseSchemaPaging.before)
|
||||
) && (
|
||||
<NextPrevious
|
||||
currentPage={currentPage}
|
||||
pageSize={PAGE_SIZE}
|
||||
paging={databaseSchemaPaging}
|
||||
pagingHandler={databaseSchemaPagingHandler}
|
||||
totalCount={databaseSchemaPaging.total}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
<Card className="h-full">
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col data-testid="description-container" span={24}>
|
||||
<Description
|
||||
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}>
|
||||
<Table
|
||||
bordered
|
||||
className="table-shadow"
|
||||
columns={tableColumn}
|
||||
data-testid="database-databaseSchemas"
|
||||
dataSource={schemaData}
|
||||
loading={{
|
||||
spinning: schemaDataLoading,
|
||||
indicator: <Loader size="small" />,
|
||||
}}
|
||||
pagination={false}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
/>
|
||||
{Boolean(
|
||||
!isNil(databaseSchemaPaging.after) ||
|
||||
!isNil(databaseSchemaPaging.before)
|
||||
) && (
|
||||
<NextPrevious
|
||||
currentPage={currentPage}
|
||||
pageSize={PAGE_SIZE}
|
||||
paging={databaseSchemaPaging}
|
||||
pagingHandler={databaseSchemaPagingHandler}
|
||||
totalCount={databaseSchemaPaging.total}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
)}
|
||||
{activeTab === 2 && (
|
||||
<Card className="p-t-xss p-b-md">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user