mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-02 03:29:03 +00:00
UI : Refactoring of TopicDetails Page (#888)
* UI : Refactoring of TopicDetails Page * changed page name in routes * addressing review comment * added toast notification for error * minor changes * minor fix * minor fix
This commit is contained in:
parent
2787ff1b17
commit
53eb2aad21
@ -0,0 +1,309 @@
|
||||
import { EntityTags } from 'Models';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { getTeamDetailsPath } from '../../constants/constants';
|
||||
import { Topic } from '../../generated/entity/data/topic';
|
||||
import { User } from '../../generated/entity/teams/user';
|
||||
import { LabelType, State } from '../../generated/type/tagLabel';
|
||||
import { useAuth } from '../../hooks/authHooks';
|
||||
import { getCurrentUserId, getUserTeams } from '../../utils/CommonUtils';
|
||||
import { bytesToSize } from '../../utils/StringsUtils';
|
||||
import { getTagsWithoutTier } from '../../utils/TableUtils';
|
||||
import Description from '../common/description/Description';
|
||||
import EntityPageInfo from '../common/entityPageInfo/EntityPageInfo';
|
||||
import TabsPane from '../common/TabsPane/TabsPane';
|
||||
import PageContainer from '../containers/PageContainer';
|
||||
import ManageTabComponent from '../ManageTab/ManageTab.component';
|
||||
import SchemaEditor from '../schema-editor/SchemaEditor';
|
||||
import { TopicDetailsProps } from './TopicDetails.interface';
|
||||
|
||||
const TopicDetails: React.FC<TopicDetailsProps> = ({
|
||||
users,
|
||||
topicDetails,
|
||||
partitions,
|
||||
cleanupPolicies,
|
||||
maximumMessageSize,
|
||||
replicationFactor,
|
||||
retentionSize,
|
||||
schemaText,
|
||||
schemaType,
|
||||
tagList,
|
||||
topicTags,
|
||||
activeTab,
|
||||
entityName,
|
||||
owner,
|
||||
description,
|
||||
tier,
|
||||
followers,
|
||||
slashedTopicName,
|
||||
setActiveTabHandler,
|
||||
settingsUpdateHandler,
|
||||
followTopicHandler,
|
||||
unfollowTopicHandler,
|
||||
descriptionUpdateHandler,
|
||||
tagUpdateHandler,
|
||||
}: TopicDetailsProps) => {
|
||||
const { isAuthDisabled } = useAuth();
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [followersCount, setFollowersCount] = useState(0);
|
||||
const [isFollowing, setIsFollowing] = useState(false);
|
||||
|
||||
const hasEditAccess = () => {
|
||||
if (owner?.type === 'user') {
|
||||
return owner.id === getCurrentUserId();
|
||||
} else {
|
||||
return getUserTeams().some((team) => team.id === owner?.id);
|
||||
}
|
||||
};
|
||||
const setFollowersData = (followers: Array<User>) => {
|
||||
setIsFollowing(
|
||||
followers.some(({ id }: { id: string }) => id === getCurrentUserId())
|
||||
);
|
||||
setFollowersCount(followers?.length);
|
||||
};
|
||||
|
||||
const getConfigDetails = () => {
|
||||
return [
|
||||
{ key: 'Partitions', value: partitions },
|
||||
{ key: 'Replication Factor', value: replicationFactor },
|
||||
{ key: 'Retention Size', value: bytesToSize(retentionSize) },
|
||||
{ key: 'CleanUp Policies', value: cleanupPolicies.join(',') },
|
||||
{ key: 'Max Message Size', value: bytesToSize(maximumMessageSize) },
|
||||
];
|
||||
};
|
||||
|
||||
const getConfigObject = () => {
|
||||
return {
|
||||
Partitions: partitions,
|
||||
'Replication Factor': replicationFactor,
|
||||
'Retention Size': retentionSize,
|
||||
'CleanUp Policies': cleanupPolicies,
|
||||
'Max Message Size': maximumMessageSize,
|
||||
};
|
||||
};
|
||||
const tabs = [
|
||||
{
|
||||
name: 'Schema',
|
||||
icon: {
|
||||
alt: 'schema',
|
||||
name: 'icon-schema',
|
||||
title: 'Schema',
|
||||
},
|
||||
isProtected: false,
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
name: 'Config',
|
||||
icon: {
|
||||
alt: 'config',
|
||||
name: 'icon-config',
|
||||
title: 'Config',
|
||||
},
|
||||
isProtected: false,
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
name: 'Manage',
|
||||
icon: {
|
||||
alt: 'manage',
|
||||
name: 'icon-manage',
|
||||
title: 'Manage',
|
||||
},
|
||||
isProtected: true,
|
||||
protectedState: !owner || hasEditAccess(),
|
||||
position: 3,
|
||||
},
|
||||
];
|
||||
const extraInfo = [
|
||||
{
|
||||
key: 'Owner',
|
||||
value:
|
||||
owner?.type === 'team'
|
||||
? getTeamDetailsPath(owner?.name || '')
|
||||
: owner?.name || '',
|
||||
placeholderText: owner?.displayName || '',
|
||||
isLink: owner?.type === 'team',
|
||||
openInNewTab: false,
|
||||
},
|
||||
{ key: 'Tier', value: tier ? tier.split('.')[1] : '' },
|
||||
...getConfigDetails(),
|
||||
];
|
||||
|
||||
const onDescriptionEdit = (): void => {
|
||||
setIsEdit(true);
|
||||
};
|
||||
const onCancel = () => {
|
||||
setIsEdit(false);
|
||||
};
|
||||
|
||||
const onDescriptionUpdate = (updatedHTML: string) => {
|
||||
if (description !== updatedHTML) {
|
||||
const updatedTopicDetails = {
|
||||
...topicDetails,
|
||||
description: updatedHTML,
|
||||
};
|
||||
descriptionUpdateHandler(updatedTopicDetails);
|
||||
setIsEdit(false);
|
||||
} else {
|
||||
setIsEdit(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onSettingsUpdate = (newOwner?: Topic['owner'], newTier?: string) => {
|
||||
if (newOwner || newTier) {
|
||||
const tierTag: Topic['tags'] = newTier
|
||||
? [
|
||||
...getTagsWithoutTier(topicDetails.tags as Array<EntityTags>),
|
||||
{
|
||||
tagFQN: newTier,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
]
|
||||
: topicDetails.tags;
|
||||
const updatedTopicDetails = {
|
||||
...topicDetails,
|
||||
owner: newOwner
|
||||
? {
|
||||
...topicDetails.owner,
|
||||
...newOwner,
|
||||
}
|
||||
: topicDetails.owner,
|
||||
tags: tierTag,
|
||||
};
|
||||
|
||||
return settingsUpdateHandler(updatedTopicDetails);
|
||||
} else {
|
||||
return Promise.reject();
|
||||
}
|
||||
};
|
||||
|
||||
const followTopic = () => {
|
||||
if (isFollowing) {
|
||||
setFollowersCount((preValu) => preValu - 1);
|
||||
setIsFollowing(false);
|
||||
unfollowTopicHandler();
|
||||
} else {
|
||||
setFollowersCount((preValu) => preValu + 1);
|
||||
setIsFollowing(true);
|
||||
followTopicHandler();
|
||||
}
|
||||
};
|
||||
|
||||
const getInfoBadge = (infos: Array<Record<string, string | number>>) => {
|
||||
return (
|
||||
<div className="tw-flex tw-justify-between">
|
||||
<div className="tw-flex tw-gap-3">
|
||||
{infos.map((info, index) => (
|
||||
<div className="tw-mt-4" key={index}>
|
||||
<span className="tw-py-1.5 tw-px-2 tw-rounded-l tw-bg-tag ">
|
||||
{info.key}
|
||||
</span>
|
||||
<span className="tw-py-1.5 tw-px-2 tw-bg-primary-lite tw-font-normal tw-rounded-r">
|
||||
{info.value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const onTagUpdate = (selectedTags?: Array<string>) => {
|
||||
if (selectedTags) {
|
||||
const prevTags =
|
||||
topicDetails?.tags?.filter((tag) =>
|
||||
selectedTags.includes(tag?.tagFQN as string)
|
||||
) || [];
|
||||
const newTags = selectedTags
|
||||
.filter((tag) => {
|
||||
return !prevTags?.map((prevTag) => prevTag.tagFQN).includes(tag);
|
||||
})
|
||||
.map((tag) => ({
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
tagFQN: tag,
|
||||
}));
|
||||
const updatedTags = [...prevTags, ...newTags];
|
||||
const updatedTopic = { ...topicDetails, tags: updatedTags };
|
||||
tagUpdateHandler(updatedTopic);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthDisabled && users.length && followers.length) {
|
||||
setFollowersData(followers);
|
||||
}
|
||||
}, [users, followers]);
|
||||
|
||||
useEffect(() => {
|
||||
setFollowersData(followers);
|
||||
}, [followers]);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className="tw-px-4 w-full">
|
||||
<EntityPageInfo
|
||||
isTagEditable
|
||||
entityName={entityName}
|
||||
extraInfo={extraInfo}
|
||||
followers={followersCount}
|
||||
followersList={followers}
|
||||
followHandler={followTopic}
|
||||
hasEditAccess={hasEditAccess()}
|
||||
isFollowing={isFollowing}
|
||||
owner={owner}
|
||||
tagList={tagList}
|
||||
tags={topicTags}
|
||||
tagsHandler={onTagUpdate}
|
||||
tier={tier ?? ''}
|
||||
titleLinks={slashedTopicName}
|
||||
/>
|
||||
<div className="tw-block tw-mt-1">
|
||||
<TabsPane
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTabHandler}
|
||||
tabs={tabs}
|
||||
/>
|
||||
|
||||
<div className="tw-bg-white tw--mx-4 tw-p-4 tw-min-h-tab">
|
||||
{activeTab === 1 && (
|
||||
<>
|
||||
<div className="tw-grid tw-grid-cols-4 tw-gap-4 w-full">
|
||||
<div className="tw-col-span-full">
|
||||
<Description
|
||||
description={description}
|
||||
hasEditAccess={hasEditAccess()}
|
||||
isEdit={isEdit}
|
||||
owner={owner}
|
||||
onCancel={onCancel}
|
||||
onDescriptionEdit={onDescriptionEdit}
|
||||
onDescriptionUpdate={onDescriptionUpdate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{getInfoBadge([{ key: 'Schema', value: schemaType }])}
|
||||
<div className="tw-my-4 tw-border tw-border-main tw-rounded-md tw-py-4">
|
||||
<SchemaEditor value={schemaText} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{activeTab === 3 && (
|
||||
<ManageTabComponent
|
||||
currentTier={tier}
|
||||
currentUser={owner?.id}
|
||||
hasEditAccess={hasEditAccess()}
|
||||
onSave={onSettingsUpdate}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 2 && (
|
||||
<SchemaEditor value={JSON.stringify(getConfigObject())} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopicDetails;
|
||||
@ -0,0 +1,31 @@
|
||||
import { EntityTags, TableDetail } from 'Models';
|
||||
import { Topic } from '../../generated/entity/data/topic';
|
||||
import { User } from '../../generated/entity/teams/user';
|
||||
import { TitleBreadcrumbProps } from '../common/title-breadcrumb/title-breadcrumb.interface';
|
||||
|
||||
export interface TopicDetailsProps {
|
||||
tagList: Array<string>;
|
||||
schemaText: string;
|
||||
schemaType: string;
|
||||
partitions: number;
|
||||
cleanupPolicies: Array<string>;
|
||||
maximumMessageSize: number;
|
||||
replicationFactor: number;
|
||||
retentionSize: number;
|
||||
users: Array<User>;
|
||||
topicDetails: Topic;
|
||||
entityName: string;
|
||||
activeTab: number;
|
||||
owner: TableDetail['owner'];
|
||||
description: string;
|
||||
tier: string;
|
||||
followers: Array<User>;
|
||||
topicTags: Array<EntityTags>;
|
||||
slashedTopicName: TitleBreadcrumbProps['titleLinks'];
|
||||
setActiveTabHandler: (value: number) => void;
|
||||
followTopicHandler: () => void;
|
||||
unfollowTopicHandler: () => void;
|
||||
settingsUpdateHandler: (updatedTopic: Topic) => Promise<void>;
|
||||
descriptionUpdateHandler: (updatedTopic: Topic) => void;
|
||||
tagUpdateHandler: (updatedTopic: Topic) => void;
|
||||
}
|
||||
@ -0,0 +1,249 @@
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { compare } from 'fast-json-patch';
|
||||
import { observer } from 'mobx-react';
|
||||
import { EntityTags, TableDetail } from 'Models';
|
||||
import React, { FunctionComponent, useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import AppState from '../../AppState';
|
||||
import { getServiceById } from '../../axiosAPIs/serviceAPI';
|
||||
import {
|
||||
addFollower,
|
||||
getTopicByFqn,
|
||||
patchTopicDetails,
|
||||
removeFollower,
|
||||
} from '../../axiosAPIs/topicsAPI';
|
||||
import { TitleBreadcrumbProps } from '../../components/common/title-breadcrumb/title-breadcrumb.interface';
|
||||
import Loader from '../../components/Loader/Loader';
|
||||
import TopicDetails from '../../components/TopicDetails/TopicDetails.component';
|
||||
import { getServiceDetailsPath } from '../../constants/constants';
|
||||
import { EntityType } from '../../enums/entity.enum';
|
||||
import { Topic } from '../../generated/entity/data/topic';
|
||||
import { User } from '../../generated/entity/teams/user';
|
||||
import useToastContext from '../../hooks/useToastContext';
|
||||
import { addToRecentViewed, getCurrentUserId } from '../../utils/CommonUtils';
|
||||
import { serviceTypeLogo } from '../../utils/ServiceUtils';
|
||||
import {
|
||||
getOwnerFromId,
|
||||
getTagsWithoutTier,
|
||||
getTierFromTableTags,
|
||||
} from '../../utils/TableUtils';
|
||||
import { getTagCategories, getTaglist } from '../../utils/TagsUtils';
|
||||
|
||||
const TopicDetailsPage: FunctionComponent = () => {
|
||||
const USERId = getCurrentUserId();
|
||||
const showToast = useToastContext();
|
||||
|
||||
const [tagList, setTagList] = useState<Array<string>>([]);
|
||||
const { topicFQN } = useParams() as Record<string, string>;
|
||||
const [topicDetails, setTopicDetails] = useState<Topic>({} as Topic);
|
||||
const [topicId, setTopicId] = useState<string>('');
|
||||
const [isLoading, setLoading] = useState<boolean>(false);
|
||||
const [description, setDescription] = useState<string>('');
|
||||
const [followers, setFollowers] = useState<Array<User>>([]);
|
||||
const [owner, setOwner] = useState<TableDetail['owner']>();
|
||||
const [tier, setTier] = useState<string>();
|
||||
const [schemaType, setSchemaType] = useState<string>('');
|
||||
const [tags, setTags] = useState<Array<EntityTags>>([]);
|
||||
const [activeTab, setActiveTab] = useState<number>(1);
|
||||
const [partitions, setPartitions] = useState<number>(0);
|
||||
const [cleanupPolicies, setCleanupPolicies] = useState<Array<string>>([]);
|
||||
const [maximumMessageSize, setMaximumMessageSize] = useState<number>(0);
|
||||
const [replicationFactor, setReplicationFactor] = useState<number>(0);
|
||||
const [retentionSize, setRetentionSize] = useState<number>(0);
|
||||
const [name, setName] = useState<string>('');
|
||||
|
||||
const [schemaText, setSchemaText] = useState<string>('{}');
|
||||
const [slashedTopicName, setSlashedTopicName] = useState<
|
||||
TitleBreadcrumbProps['titleLinks']
|
||||
>([]);
|
||||
|
||||
const activeTabHandler = (tabValue: number) => {
|
||||
setActiveTab(tabValue);
|
||||
};
|
||||
|
||||
const saveUpdatedTopicData = (updatedData: Topic): Promise<AxiosResponse> => {
|
||||
const jsonPatch = compare(topicDetails, updatedData);
|
||||
|
||||
return patchTopicDetails(
|
||||
topicId,
|
||||
jsonPatch
|
||||
) as unknown as Promise<AxiosResponse>;
|
||||
};
|
||||
|
||||
const fetchTags = () => {
|
||||
getTagCategories().then((res) => {
|
||||
setTagList(getTaglist(res.data));
|
||||
});
|
||||
};
|
||||
|
||||
const fetchTopicDetail = (topicFQN: string) => {
|
||||
setLoading(true);
|
||||
getTopicByFqn(topicFQN, ['owner', 'service', 'followers', 'tags'])
|
||||
.then((res: AxiosResponse) => {
|
||||
const {
|
||||
id,
|
||||
description,
|
||||
followers,
|
||||
fullyQualifiedName,
|
||||
name,
|
||||
schemaType,
|
||||
schemaText,
|
||||
service,
|
||||
tags,
|
||||
owner,
|
||||
partitions,
|
||||
cleanupPolicies,
|
||||
maximumMessageSize,
|
||||
replicationFactor,
|
||||
retentionSize,
|
||||
} = res.data;
|
||||
setName(name);
|
||||
setTopicDetails(res.data);
|
||||
setTopicId(id);
|
||||
setDescription(description ?? '');
|
||||
setSchemaType(schemaType);
|
||||
setFollowers(followers);
|
||||
setOwner(getOwnerFromId(owner?.id));
|
||||
setTier(getTierFromTableTags(tags));
|
||||
setTags(getTagsWithoutTier(tags));
|
||||
setSchemaText(schemaText);
|
||||
setPartitions(partitions);
|
||||
setCleanupPolicies(cleanupPolicies);
|
||||
setMaximumMessageSize(maximumMessageSize);
|
||||
setReplicationFactor(replicationFactor);
|
||||
setRetentionSize(retentionSize);
|
||||
getServiceById('messagingServices', service?.id)
|
||||
.then((serviceRes: AxiosResponse) => {
|
||||
setSlashedTopicName([
|
||||
{
|
||||
name: serviceRes.data.name,
|
||||
url: serviceRes.data.name
|
||||
? getServiceDetailsPath(
|
||||
serviceRes.data.name,
|
||||
serviceRes.data.serviceType
|
||||
)
|
||||
: '',
|
||||
imgSrc: serviceRes.data.serviceType
|
||||
? serviceTypeLogo(serviceRes.data.serviceType)
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
name: name,
|
||||
url: '',
|
||||
activeTitle: true,
|
||||
},
|
||||
]);
|
||||
|
||||
addToRecentViewed({
|
||||
entityType: EntityType.TOPIC,
|
||||
fqn: fullyQualifiedName,
|
||||
serviceType: serviceRes.data.serviceType,
|
||||
timestamp: 0,
|
||||
});
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
const errMsg =
|
||||
err.message || `Error while fetching service for ${name}`;
|
||||
showToast({
|
||||
variant: 'error',
|
||||
body: errMsg,
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
const errMsg = err.message || 'Error while fetching topic details';
|
||||
showToast({
|
||||
variant: 'error',
|
||||
body: errMsg,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const followTopic = () => {
|
||||
addFollower(topicId, USERId).then((res: AxiosResponse) => {
|
||||
const { followers } = res.data;
|
||||
setFollowers(followers);
|
||||
});
|
||||
};
|
||||
const unfollowTopic = () => {
|
||||
removeFollower(topicId, USERId).then((res: AxiosResponse) => {
|
||||
const { followers } = res.data;
|
||||
|
||||
setFollowers(followers);
|
||||
});
|
||||
};
|
||||
|
||||
const descriptionUpdateHandler = (updatedTopic: Topic) => {
|
||||
saveUpdatedTopicData(updatedTopic).then((res: AxiosResponse) => {
|
||||
const { description } = res.data;
|
||||
setTopicDetails(res.data);
|
||||
setDescription(description);
|
||||
});
|
||||
};
|
||||
|
||||
const settingsUpdateHandler = (updatedTopic: Topic): Promise<void> => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
saveUpdatedTopicData(updatedTopic)
|
||||
.then((res) => {
|
||||
setTopicDetails(res.data);
|
||||
setOwner(getOwnerFromId(res.data.owner?.id));
|
||||
setTier(getTierFromTableTags(res.data.tags));
|
||||
resolve();
|
||||
})
|
||||
.catch(() => reject());
|
||||
});
|
||||
};
|
||||
|
||||
const onTagUpdate = (updatedTopic: Topic) => {
|
||||
saveUpdatedTopicData(updatedTopic).then((res: AxiosResponse) => {
|
||||
setTier(getTierFromTableTags(res.data.tags));
|
||||
setTags(getTagsWithoutTier(res.data.tags));
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTopicDetail(topicFQN);
|
||||
}, [topicFQN]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTags();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<TopicDetails
|
||||
activeTab={activeTab}
|
||||
cleanupPolicies={cleanupPolicies}
|
||||
description={description}
|
||||
descriptionUpdateHandler={descriptionUpdateHandler}
|
||||
entityName={name}
|
||||
followers={followers}
|
||||
followTopicHandler={followTopic}
|
||||
maximumMessageSize={maximumMessageSize}
|
||||
owner={owner}
|
||||
partitions={partitions}
|
||||
replicationFactor={replicationFactor}
|
||||
retentionSize={retentionSize}
|
||||
schemaText={schemaText}
|
||||
schemaType={schemaType}
|
||||
setActiveTabHandler={activeTabHandler}
|
||||
settingsUpdateHandler={settingsUpdateHandler}
|
||||
slashedTopicName={slashedTopicName}
|
||||
tagList={tagList}
|
||||
tagUpdateHandler={onTagUpdate}
|
||||
tier={tier as string}
|
||||
topicDetails={topicDetails}
|
||||
topicTags={tags}
|
||||
unfollowTopicHandler={unfollowTopic}
|
||||
users={AppState.users}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(TopicDetailsPage);
|
||||
@ -242,7 +242,7 @@ const ServicesPage = () => {
|
||||
.catch((err: AxiosError) => {
|
||||
showToast({
|
||||
variant: 'error',
|
||||
body: err.response?.data?.responseMessage ?? 'Something went wrong!',
|
||||
body: err.message || 'Something went wrong!',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,430 +0,0 @@
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { compare } from 'fast-json-patch';
|
||||
import { ColumnTags, TableDetail, Topic } from 'Models';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import AppState from '../../AppState';
|
||||
import { getServiceById } from '../../axiosAPIs/serviceAPI';
|
||||
import {
|
||||
addFollower,
|
||||
getTopicByFqn,
|
||||
patchTopicDetails,
|
||||
removeFollower,
|
||||
} from '../../axiosAPIs/topicsAPI';
|
||||
import Description from '../../components/common/description/Description';
|
||||
import EntityPageInfo from '../../components/common/entityPageInfo/EntityPageInfo';
|
||||
import TabsPane from '../../components/common/TabsPane/TabsPane';
|
||||
import { TitleBreadcrumbProps } from '../../components/common/title-breadcrumb/title-breadcrumb.interface';
|
||||
import PageContainer from '../../components/containers/PageContainer';
|
||||
import Loader from '../../components/Loader/Loader';
|
||||
import ManageTab from '../../components/ManageTab/ManageTab.component';
|
||||
import SchemaEditor from '../../components/schema-editor/SchemaEditor';
|
||||
import {
|
||||
getServiceDetailsPath,
|
||||
getTeamDetailsPath,
|
||||
} from '../../constants/constants';
|
||||
import { EntityType } from '../../enums/entity.enum';
|
||||
import { User } from '../../generated/entity/teams/user';
|
||||
import { useAuth } from '../../hooks/authHooks';
|
||||
import {
|
||||
addToRecentViewed,
|
||||
getCurrentUserId,
|
||||
getUserTeams,
|
||||
} from '../../utils/CommonUtils';
|
||||
import { serviceTypeLogo } from '../../utils/ServiceUtils';
|
||||
import { bytesToSize } from '../../utils/StringsUtils';
|
||||
import {
|
||||
getOwnerFromId,
|
||||
getTagsWithoutTier,
|
||||
getTierFromTableTags,
|
||||
} from '../../utils/TableUtils';
|
||||
import { getTagCategories, getTaglist } from '../../utils/TagsUtils';
|
||||
|
||||
const MyTopicDetailPage = () => {
|
||||
const USERId = getCurrentUserId();
|
||||
|
||||
const { isAuthDisabled } = useAuth();
|
||||
|
||||
const [tagList, setTagList] = useState<Array<string>>([]);
|
||||
const { topicFQN } = useParams() as Record<string, string>;
|
||||
const [topicDetails, setTopicDetails] = useState<Topic>({} as Topic);
|
||||
const [topicId, setTopicId] = useState<string>('');
|
||||
const [isLoading, setLoading] = useState<boolean>(false);
|
||||
const [description, setDescription] = useState<string>('');
|
||||
const [followers, setFollowers] = useState<Array<User>>([]);
|
||||
const [followersCount, setFollowersCount] = useState<number>(0);
|
||||
const [isFollowing, setIsFollowing] = useState(false);
|
||||
const [owner, setOwner] = useState<TableDetail['owner']>();
|
||||
const [tier, setTier] = useState<string>();
|
||||
const [schemaType, setSchemaType] = useState<string>('');
|
||||
const [tags, setTags] = useState<Array<ColumnTags>>([]);
|
||||
const [activeTab, setActiveTab] = useState<number>(1);
|
||||
const [partitions, setPartitions] = useState<number>(0);
|
||||
const [cleanupPolicies, setCleanupPolicies] = useState<Array<string>>([]);
|
||||
const [maximumMessageSize, setMaximumMessageSize] = useState<number>(0);
|
||||
const [replicationFactor, setReplicationFactor] = useState<number>(0);
|
||||
const [retentionSize, setRetentionSize] = useState<number>(0);
|
||||
const [name, setName] = useState<string>('');
|
||||
|
||||
const [isEdit, setIsEdit] = useState<boolean>(false);
|
||||
const [schemaText, setSchemaText] = useState<string>('{}');
|
||||
const [slashedTopicName, setSlashedTopicName] = useState<
|
||||
TitleBreadcrumbProps['titleLinks']
|
||||
>([]);
|
||||
|
||||
const hasEditAccess = () => {
|
||||
if (owner?.type === 'user') {
|
||||
return owner.id === getCurrentUserId();
|
||||
} else {
|
||||
return getUserTeams().some((team) => team.id === owner?.id);
|
||||
}
|
||||
};
|
||||
const tabs = [
|
||||
{
|
||||
name: 'Schema',
|
||||
icon: {
|
||||
alt: 'schema',
|
||||
name: 'icon-schema',
|
||||
title: 'Schema',
|
||||
},
|
||||
isProtected: false,
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
name: 'Config',
|
||||
icon: {
|
||||
alt: 'config',
|
||||
name: 'icon-config',
|
||||
title: 'Config',
|
||||
},
|
||||
isProtected: false,
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
name: 'Manage',
|
||||
icon: {
|
||||
alt: 'manage',
|
||||
name: 'icon-manage',
|
||||
title: 'Manage',
|
||||
},
|
||||
isProtected: true,
|
||||
protectedState: !owner || hasEditAccess(),
|
||||
position: 3,
|
||||
},
|
||||
];
|
||||
const fetchTags = () => {
|
||||
getTagCategories().then((res) => {
|
||||
if (res.data) {
|
||||
setTagList(getTaglist(res.data));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const setFollowersData = (followers: Array<User>) => {
|
||||
// need to check if already following or not with logedIn user id
|
||||
setIsFollowing(followers.some(({ id }: { id: string }) => id === USERId));
|
||||
setFollowersCount(followers?.length);
|
||||
};
|
||||
|
||||
const fetchTopicDetail = (topicFQN: string) => {
|
||||
setLoading(true);
|
||||
getTopicByFqn(topicFQN, ['owner', 'service', 'followers', 'tags']).then(
|
||||
(res: AxiosResponse) => {
|
||||
const {
|
||||
id,
|
||||
description,
|
||||
followers,
|
||||
fullyQualifiedName,
|
||||
name,
|
||||
schemaType,
|
||||
schemaText,
|
||||
service,
|
||||
tags,
|
||||
owner,
|
||||
partitions,
|
||||
cleanupPolicies,
|
||||
maximumMessageSize,
|
||||
replicationFactor,
|
||||
retentionSize,
|
||||
} = res.data;
|
||||
setName(name);
|
||||
setTopicDetails(res.data);
|
||||
setTopicId(id);
|
||||
setDescription(description ?? '');
|
||||
setSchemaType(schemaType);
|
||||
setFollowers(followers);
|
||||
setFollowersData(followers);
|
||||
setOwner(getOwnerFromId(owner?.id));
|
||||
setTier(getTierFromTableTags(tags));
|
||||
setTags(getTagsWithoutTier(tags));
|
||||
setSchemaText(schemaText);
|
||||
setPartitions(partitions);
|
||||
setCleanupPolicies(cleanupPolicies);
|
||||
setMaximumMessageSize(maximumMessageSize);
|
||||
setReplicationFactor(replicationFactor);
|
||||
setRetentionSize(retentionSize);
|
||||
getServiceById('messagingServices', service?.id).then(
|
||||
(serviceRes: AxiosResponse) => {
|
||||
setSlashedTopicName([
|
||||
{
|
||||
name: serviceRes.data.name,
|
||||
url: serviceRes.data.name
|
||||
? getServiceDetailsPath(
|
||||
serviceRes.data.name,
|
||||
serviceRes.data.serviceType
|
||||
)
|
||||
: '',
|
||||
imgSrc: serviceRes.data.serviceType
|
||||
? serviceTypeLogo(serviceRes.data.serviceType)
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
name: name,
|
||||
url: '',
|
||||
activeTitle: true,
|
||||
},
|
||||
]);
|
||||
|
||||
addToRecentViewed({
|
||||
entityType: EntityType.TOPIC,
|
||||
fqn: fullyQualifiedName,
|
||||
serviceType: serviceRes.data.serviceType,
|
||||
timestamp: 0,
|
||||
});
|
||||
}
|
||||
);
|
||||
setLoading(false);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const followTopic = (): void => {
|
||||
if (isFollowing) {
|
||||
removeFollower(topicId, USERId).then((res: AxiosResponse) => {
|
||||
const { followers } = res.data;
|
||||
setFollowers(followers);
|
||||
setFollowersCount((preValu) => preValu - 1);
|
||||
setIsFollowing(false);
|
||||
});
|
||||
} else {
|
||||
addFollower(topicId, USERId).then((res: AxiosResponse) => {
|
||||
const { followers } = res.data;
|
||||
setFollowers(followers);
|
||||
setFollowersCount((preValu) => preValu + 1);
|
||||
setIsFollowing(true);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onDescriptionUpdate = (updatedHTML: string) => {
|
||||
const updatedTopic = { ...topicDetails, description: updatedHTML };
|
||||
|
||||
const jsonPatch = compare(topicDetails, updatedTopic);
|
||||
patchTopicDetails(topicId, jsonPatch).then((res: AxiosResponse) => {
|
||||
setDescription(res.data.description);
|
||||
});
|
||||
setIsEdit(false);
|
||||
};
|
||||
const onDescriptionEdit = (): void => {
|
||||
setIsEdit(true);
|
||||
};
|
||||
const onCancel = () => {
|
||||
setIsEdit(false);
|
||||
};
|
||||
|
||||
const onSettingsUpdate = (
|
||||
newOwner?: TableDetail['owner'],
|
||||
newTier?: TableDetail['tier']
|
||||
): Promise<void> => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (newOwner || newTier) {
|
||||
const tierTag: TableDetail['tags'] = newTier
|
||||
? [
|
||||
...getTagsWithoutTier(topicDetails.tags),
|
||||
{ tagFQN: newTier, labelType: 'Manual', state: 'Confirmed' },
|
||||
]
|
||||
: topicDetails.tags;
|
||||
const updatedTopic = {
|
||||
...topicDetails,
|
||||
owner: newOwner
|
||||
? { ...topicDetails.owner, ...newOwner }
|
||||
: topicDetails.owner,
|
||||
tags: tierTag,
|
||||
};
|
||||
const jsonPatch = compare(topicDetails, updatedTopic);
|
||||
patchTopicDetails(topicId, jsonPatch)
|
||||
.then((res: AxiosResponse) => {
|
||||
setTopicDetails(res.data);
|
||||
setOwner(getOwnerFromId(res.data.owner?.id));
|
||||
setTier(getTierFromTableTags(res.data.tags));
|
||||
resolve();
|
||||
})
|
||||
.catch(() => reject());
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onTagUpdate = (selectedTags?: Array<string>) => {
|
||||
if (selectedTags) {
|
||||
const prevTags = topicDetails.tags.filter((tag) =>
|
||||
selectedTags.includes(tag.tagFQN)
|
||||
);
|
||||
const newTags: Array<ColumnTags> = selectedTags
|
||||
.filter((tag) => {
|
||||
return !prevTags.map((prevTag) => prevTag.tagFQN).includes(tag);
|
||||
})
|
||||
.map((tag) => ({
|
||||
labelType: 'Manual',
|
||||
state: 'Confirmed',
|
||||
tagFQN: tag,
|
||||
}));
|
||||
const updatedTags = [...prevTags, ...newTags];
|
||||
const updatedTopic = { ...topicDetails, tags: updatedTags };
|
||||
const jsonPatch = compare(topicDetails, updatedTopic);
|
||||
patchTopicDetails(topicId, jsonPatch).then((res: AxiosResponse) => {
|
||||
setTier(getTierFromTableTags(res.data.tags));
|
||||
setTags(getTagsWithoutTier(res.data.tags));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getInfoBadge = (infos: Array<Record<string, string | number>>) => {
|
||||
return (
|
||||
<div className="tw-flex tw-justify-between">
|
||||
<div className="tw-flex tw-gap-3">
|
||||
{infos.map((info, index) => (
|
||||
<div className="tw-mt-4" key={index}>
|
||||
<span className="tw-py-1.5 tw-px-2 tw-rounded-l tw-bg-tag ">
|
||||
{info.key}
|
||||
</span>
|
||||
<span className="tw-py-1.5 tw-px-2 tw-bg-primary-lite tw-font-normal tw-rounded-r">
|
||||
{info.value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getConfigDetails = () => {
|
||||
return [
|
||||
{ key: 'Partitions', value: partitions },
|
||||
{ key: 'Replication Factor', value: replicationFactor },
|
||||
{ key: 'Retention Size', value: bytesToSize(retentionSize) },
|
||||
{ key: 'CleanUp Policies', value: cleanupPolicies.join(',') },
|
||||
{ key: 'Max Message Size', value: bytesToSize(maximumMessageSize) },
|
||||
];
|
||||
};
|
||||
|
||||
const getConfigObject = () => {
|
||||
return {
|
||||
Partitions: partitions,
|
||||
'Replication Factor': replicationFactor,
|
||||
'Retention Size': retentionSize,
|
||||
'CleanUp Policies': cleanupPolicies,
|
||||
'Max Message Size': maximumMessageSize,
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTopicDetail(topicFQN);
|
||||
}, [topicFQN]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthDisabled && AppState.users.length && followers.length) {
|
||||
setFollowersData(followers);
|
||||
}
|
||||
}, [AppState.users, followers]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTags();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
{isLoading ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<div className="tw-px-4 w-full">
|
||||
<EntityPageInfo
|
||||
isTagEditable
|
||||
entityName={name}
|
||||
extraInfo={[
|
||||
{
|
||||
key: 'Owner',
|
||||
value:
|
||||
owner?.type === 'team'
|
||||
? getTeamDetailsPath(owner?.name || '')
|
||||
: owner?.name || '',
|
||||
placeholderText: owner?.displayName || '',
|
||||
isLink: owner?.type === 'team',
|
||||
openInNewTab: false,
|
||||
},
|
||||
{ key: 'Tier', value: tier ? tier.split('.')[1] : '' },
|
||||
...getConfigDetails(),
|
||||
]}
|
||||
followers={followersCount}
|
||||
followersList={followers}
|
||||
followHandler={followTopic}
|
||||
hasEditAccess={hasEditAccess()}
|
||||
isFollowing={isFollowing}
|
||||
owner={owner}
|
||||
tagList={tagList}
|
||||
tags={tags}
|
||||
tagsHandler={onTagUpdate}
|
||||
tier={tier ?? ''}
|
||||
titleLinks={slashedTopicName}
|
||||
/>
|
||||
<div className="tw-block tw-mt-1">
|
||||
<TabsPane
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
tabs={tabs}
|
||||
/>
|
||||
|
||||
<div className="tw-bg-white tw--mx-4 tw-p-4 tw-min-h-tab">
|
||||
{activeTab === 1 && (
|
||||
<>
|
||||
<div className="tw-grid tw-grid-cols-4 tw-gap-4 w-full">
|
||||
<div className="tw-col-span-full">
|
||||
<Description
|
||||
description={description}
|
||||
hasEditAccess={hasEditAccess()}
|
||||
isEdit={isEdit}
|
||||
owner={owner}
|
||||
onCancel={onCancel}
|
||||
onDescriptionEdit={onDescriptionEdit}
|
||||
onDescriptionUpdate={onDescriptionUpdate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{getInfoBadge([{ key: 'Schema', value: schemaType }])}
|
||||
<div className="tw-my-4 tw-border tw-border-main tw-rounded-md tw-py-4">
|
||||
<SchemaEditor value={schemaText} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{activeTab === 3 && (
|
||||
<ManageTab
|
||||
currentTier={tier}
|
||||
currentUser={owner?.id}
|
||||
hasEditAccess={hasEditAccess()}
|
||||
onSave={onSettingsUpdate}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 2 && (
|
||||
<SchemaEditor value={JSON.stringify(getConfigObject())} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyTopicDetailPage;
|
||||
@ -38,7 +38,7 @@ import StorePage from '../pages/store';
|
||||
import SwaggerPage from '../pages/swagger';
|
||||
import TagsPage from '../pages/tags';
|
||||
import TeamsPage from '../pages/teams';
|
||||
import MyTopicDetailPage from '../pages/topic-details';
|
||||
import TopicDetailsPage from '../pages/TopicDetails/TopicDetailsPage.component';
|
||||
import TourPage from '../pages/tour-page';
|
||||
import UsersPage from '../pages/users';
|
||||
import WorkflowsPage from '../pages/workflows';
|
||||
@ -69,7 +69,7 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
|
||||
<Route exact component={TagsPage} path={ROUTES.TAGS} />
|
||||
<Route component={DatabaseDetails} path={ROUTES.DATABASE_DETAILS} />
|
||||
<Route component={DatasetDetailsPage} path={ROUTES.DATASET_DETAILS} />
|
||||
<Route component={MyTopicDetailPage} path={ROUTES.TOPIC_DETAILS} />
|
||||
<Route component={TopicDetailsPage} path={ROUTES.TOPIC_DETAILS} />
|
||||
<Route component={MyDashBoardPage} path={ROUTES.DASHBOARD_DETAILS} />
|
||||
<Route component={MyPipelinePage} path={ROUTES.PIPELINE_DETAILS} />
|
||||
<Route component={Onboarding} path={ROUTES.ONBOARDING} />
|
||||
|
||||
@ -40,12 +40,14 @@ export const getTagCategories = async (fields?: Array<string> | string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getTaglist = (categories: Array<TagCategory>): Array<string> => {
|
||||
export const getTaglist = (
|
||||
categories: Array<TagCategory> = []
|
||||
): Array<string> => {
|
||||
const children = categories.map((category: TagCategory) => {
|
||||
return category.children || [];
|
||||
});
|
||||
const allChildren = flatten(children);
|
||||
const tagList = (allChildren as unknown as TagClass[])?.map((tag) => {
|
||||
const tagList = (allChildren as unknown as TagClass[]).map((tag) => {
|
||||
return tag?.fullyQualifiedName || '';
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user