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:
Chirag Madlani 2023-04-18 00:07:22 +05:30 committed by GitHub
parent 072dfd199c
commit af853d7b4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 292 additions and 144 deletions

View File

@ -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}

View File

@ -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,
},
]);

View File

@ -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({

View File

@ -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,
},
]);

View File

@ -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,
},
]);

View File

@ -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', () => ({

View File

@ -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">