From 4d3ec274ea05419473531dd516cc37aaec5bb121 Mon Sep 17 00:00:00 2001 From: Sachin Chaurasiya Date: Fri, 3 Sep 2021 09:31:57 +0530 Subject: [PATCH] Adding topics details page (#369) * Adding topics details page * added spport for Avro and json mode . * minor style changes * addressing review comment * addressing review comment --- .../src/main/resources/ui/package-lock.json | 10 + .../src/main/resources/ui/package.json | 2 + .../resources/ui/src/axiosAPIs/topicsAPI.ts | 46 +++ .../SchemaTreeStructure.css | 47 +++ .../SchemaTreeStructure.tsx | 107 ++++++ .../components/common/TabsPane/TabsPane.tsx | 2 + .../common/entityPageInfo/EntityPageInfo.tsx | 132 +++++-- .../my-data-details/SchemaTable.tsx | 6 +- .../components/schema-editor/SchemaEditor.tsx | 56 +++ .../schema-editor/SchemaEditor.utils.ts | 24 ++ .../tags-container.interface.ts | 1 + .../tags-container/tags-container.tsx | 6 +- .../ui/src/components/tags/tags.interface.ts | 1 + .../resources/ui/src/components/tags/tags.tsx | 3 +- .../resources/ui/src/constants/constants.ts | 1 + .../resources/ui/src/interface/types.d.ts | 48 +++ .../ui/src/pages/my-data-details/index.tsx | 2 +- .../resources/ui/src/pages/tags/index.tsx | 2 +- .../ui/src/pages/topic-details/index.tsx | 332 ++++++++++++++++++ .../ui/src/router/AuthenticatedAppRouter.tsx | 3 +- .../main/resources/ui/src/styles/x-master.css | 4 + 21 files changed, 790 insertions(+), 45 deletions(-) create mode 100644 catalog-rest-service/src/main/resources/ui/src/components/common/SchemaTreeStructure/SchemaTreeStructure.css create mode 100644 catalog-rest-service/src/main/resources/ui/src/components/common/SchemaTreeStructure/SchemaTreeStructure.tsx create mode 100644 catalog-rest-service/src/main/resources/ui/src/components/schema-editor/SchemaEditor.tsx create mode 100644 catalog-rest-service/src/main/resources/ui/src/components/schema-editor/SchemaEditor.utils.ts create mode 100644 catalog-rest-service/src/main/resources/ui/src/pages/topic-details/index.tsx diff --git a/catalog-rest-service/src/main/resources/ui/package-lock.json b/catalog-rest-service/src/main/resources/ui/package-lock.json index 7f49b6599dd..80c0a80b23a 100644 --- a/catalog-rest-service/src/main/resources/ui/package-lock.json +++ b/catalog-rest-service/src/main/resources/ui/package-lock.json @@ -5767,6 +5767,11 @@ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, + "codemirror": { + "version": "5.62.3", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.62.3.tgz", + "integrity": "sha512-zZAyOfN8TU67ngqrxhOgtkSAGV9jSpN1snbl8elPtnh9Z5A11daR405+dhLzLnuXrwX0WCShWlybxPN3QC/9Pg==" + }, "collect-v8-coverage": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", @@ -17262,6 +17267,11 @@ "warning": "^4.0.3" } }, + "react-codemirror2": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/react-codemirror2/-/react-codemirror2-7.2.1.tgz", + "integrity": "sha512-t7YFmz1AXdlImgHXA9Ja0T6AWuopilub24jRaQdPVbzUJVNKIYuy3uCFZYa7CE5S3UW6SrSa5nAqVQvtzRF9gw==" + }, "react-dom": { "version": "16.14.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", diff --git a/catalog-rest-service/src/main/resources/ui/package.json b/catalog-rest-service/src/main/resources/ui/package.json index 8b93a0e11ba..764180c6715 100644 --- a/catalog-rest-service/src/main/resources/ui/package.json +++ b/catalog-rest-service/src/main/resources/ui/package.json @@ -23,6 +23,7 @@ "bootstrap": "^4.5.2", "buffer": "^6.0.3", "builtin-status-codes": "^3.0.0", + "codemirror": "^5.62.3", "cookie-storage": "^6.1.0", "core-js": "^3.10.1", "draft-js": "^0.11.7", @@ -53,6 +54,7 @@ "prop-types": "^15.7.2", "react": "^16.14.0", "react-bootstrap": "^1.6.0", + "react-codemirror2": "^7.2.1", "react-dom": "^16.14.0", "react-draft-wysiwyg": "^1.14.7", "react-js-pagination": "^3.0.3", diff --git a/catalog-rest-service/src/main/resources/ui/src/axiosAPIs/topicsAPI.ts b/catalog-rest-service/src/main/resources/ui/src/axiosAPIs/topicsAPI.ts index 8163439e1b3..47646622957 100644 --- a/catalog-rest-service/src/main/resources/ui/src/axiosAPIs/topicsAPI.ts +++ b/catalog-rest-service/src/main/resources/ui/src/axiosAPIs/topicsAPI.ts @@ -16,6 +16,7 @@ */ import { AxiosResponse } from 'axios'; +import { Topic } from 'Models'; import { getURLWithQueryFields } from '../utils/APIUtils'; import APIClient from './index'; @@ -31,3 +32,48 @@ export const getTopics: Function = ( return APIClient.get(url); }; + +export const getTopicByFqn: Function = ( + fqn: string, + arrQueryFields: string +): Promise => { + const url = getURLWithQueryFields(`/topics/name/${fqn}`, arrQueryFields); + + return APIClient.get(url); +}; + +export const addFollower: Function = ( + topicId: string, + userId: string +): Promise => { + const configOptions = { + headers: { 'Content-type': 'application/json' }, + }; + + return APIClient.put(`/topics/${topicId}/followers`, userId, configOptions); +}; + +export const removeFollower: Function = ( + topicId: string, + userId: string +): Promise => { + const configOptions = { + headers: { 'Content-type': 'application/json' }, + }; + + return APIClient.delete( + `/topics/${topicId}/followers/${userId}`, + configOptions + ); +}; + +export const patchTopicDetails: Function = ( + id: string, + data: Topic +): Promise => { + const configOptions = { + headers: { 'Content-type': 'application/json-patch+json' }, + }; + + return APIClient.patch(`/topics/${id}`, data, configOptions); +}; diff --git a/catalog-rest-service/src/main/resources/ui/src/components/common/SchemaTreeStructure/SchemaTreeStructure.css b/catalog-rest-service/src/main/resources/ui/src/components/common/SchemaTreeStructure/SchemaTreeStructure.css new file mode 100644 index 00000000000..53290cb012d --- /dev/null +++ b/catalog-rest-service/src/main/resources/ui/src/components/common/SchemaTreeStructure/SchemaTreeStructure.css @@ -0,0 +1,47 @@ +.field-wrapper { + display: flex; + flex-direction: column; + gap: 2px; + margin-left: 15px; + border-left: 3px solid #d9ceee; + padding-top: 10px; + padding-bottom: 10px; +} + +.field-child { + display: flex; + gap: 4px; +} +.field-child::before { + content: ''; + display: inline-block; + width: 32px; + height: 3px; + background: #d9ceee; + position: relative; + top: 11px; +} +.field-child-icon { + cursor: pointer; + color: #7147e8; +} +.field-child-icon i { + vertical-align: sub; + margin-right: 2px; + margin-left: 2px; +} + +.field-label { + display: flex; + gap: 4px; +} + +.field-label-name { + padding: 4px 6px; +} + +.child-fields-wrapper { + display: flex; + flex-direction: column; + /* margin-top: -11px; */ +} diff --git a/catalog-rest-service/src/main/resources/ui/src/components/common/SchemaTreeStructure/SchemaTreeStructure.tsx b/catalog-rest-service/src/main/resources/ui/src/components/common/SchemaTreeStructure/SchemaTreeStructure.tsx new file mode 100644 index 00000000000..6fec3ca71f3 --- /dev/null +++ b/catalog-rest-service/src/main/resources/ui/src/components/common/SchemaTreeStructure/SchemaTreeStructure.tsx @@ -0,0 +1,107 @@ +import React, { CSSProperties, useCallback, useState } from 'react'; +import './SchemaTreeStructure.css'; + +type Props = { + positions?: Array; + name: string; + type: string; + fields?: Array; + isCollapsed?: boolean; +}; + +export const getStyle = (type: string) => { + const sharedStyles = { + padding: '4px 8px', + borderRadius: '5px', + minWidth: '60px', + textAlign: 'center', + display: 'inline-block', + }; + switch (type) { + case 'double': + return { + backgroundColor: '#B02AAC33', + color: '#B02AAC', + ...sharedStyles, + }; + + case 'string': + return { + backgroundColor: '#51c41a33', + color: '#51c41a', + ...sharedStyles, + }; + + case 'int': + return { + backgroundColor: '#1890FF33', + color: '#1890FF', + ...sharedStyles, + }; + + default: + return { + backgroundColor: '#EEEAF8', + ...sharedStyles, + }; + } +}; + +const SchemaTreeStructure = ({ + name, + type, + fields, + isCollapsed = false, + // to track position of element [L0,L1,L2,...Ln] + positions = [], +}: Props) => { + const [showChildren, setShowChildren] = useState(!isCollapsed); + const flag = (fields ?? []).length > 0; + + const showChildrenHandler = useCallback(() => { + setShowChildren(!showChildren); + }, [showChildren, setShowChildren]); + + const getIcon = () => { + return ( + flag && + (showChildren ? ( + + ) : ( + + )) + ); + }; + + return ( +
+
+

+ {getIcon()} +

+

+ {type} + {name} +

+
+ {flag && showChildren && ( +
+ {(fields ?? []).map((field, index) => ( + + ))} +
+ )} +
+ ); +}; + +export default SchemaTreeStructure; diff --git a/catalog-rest-service/src/main/resources/ui/src/components/common/TabsPane/TabsPane.tsx b/catalog-rest-service/src/main/resources/ui/src/components/common/TabsPane/TabsPane.tsx index 8b17501ba7d..5eb9c6cd491 100644 --- a/catalog-rest-service/src/main/resources/ui/src/components/common/TabsPane/TabsPane.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/components/common/TabsPane/TabsPane.tsx @@ -29,6 +29,7 @@ const TabsPane = ({ activeTab, setActiveTab, tabs }: Props) => { tab.isProtected ? ( + ) : ( + + + + )} + + + )} ); diff --git a/catalog-rest-service/src/main/resources/ui/src/components/my-data-details/SchemaTable.tsx b/catalog-rest-service/src/main/resources/ui/src/components/my-data-details/SchemaTable.tsx index cbd1566f64f..1595c223726 100644 --- a/catalog-rest-service/src/main/resources/ui/src/components/my-data-details/SchemaTable.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/components/my-data-details/SchemaTable.tsx @@ -287,7 +287,7 @@ const SchemaTable: FunctionComponent = ({ @@ -386,7 +386,7 @@ const SchemaTable: FunctionComponent = ({ @@ -394,7 +394,7 @@ const SchemaTable: FunctionComponent = ({ diff --git a/catalog-rest-service/src/main/resources/ui/src/components/schema-editor/SchemaEditor.tsx b/catalog-rest-service/src/main/resources/ui/src/components/schema-editor/SchemaEditor.tsx new file mode 100644 index 00000000000..412ae881aca --- /dev/null +++ b/catalog-rest-service/src/main/resources/ui/src/components/schema-editor/SchemaEditor.tsx @@ -0,0 +1,56 @@ +import { Editor, EditorChange } from 'codemirror'; +import 'codemirror/addon/edit/closebrackets.js'; +import 'codemirror/addon/edit/matchbrackets.js'; +import 'codemirror/addon/fold/brace-fold'; +import 'codemirror/addon/fold/foldgutter.css'; +import 'codemirror/addon/fold/foldgutter.js'; +import 'codemirror/addon/selection/active-line'; +import 'codemirror/lib/codemirror.css'; +import 'codemirror/mode/javascript/javascript'; +import React, { useState } from 'react'; +import { Controlled as CodeMirror } from 'react-codemirror2'; +import { JSON_TAB_SIZE } from '../../constants/constants'; +import { getSchemaEditorValue } from './SchemaEditor.utils'; + +const options = { + tabSize: JSON_TAB_SIZE, + indentUnit: JSON_TAB_SIZE, + indentWithTabs: false, + lineNumbers: true, + lineWrapping: true, + styleActiveLine: true, + matchBrackets: true, + autoCloseBrackets: true, + foldGutter: true, + gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], + mode: { + name: 'javascript', + json: true, + }, + readOnly: true, +}; + +const SchemaEditor = ({ value }: { value: string }) => { + const [internalValue, setInternalValue] = useState( + getSchemaEditorValue(value) + ); + const handleEditorInputBeforeChange = ( + _editor: Editor, + _data: EditorChange, + value: string + ): void => { + setInternalValue(getSchemaEditorValue(value)); + }; + + return ( +
+ +
+ ); +}; + +export default SchemaEditor; diff --git a/catalog-rest-service/src/main/resources/ui/src/components/schema-editor/SchemaEditor.utils.ts b/catalog-rest-service/src/main/resources/ui/src/components/schema-editor/SchemaEditor.utils.ts new file mode 100644 index 00000000000..21da5f1694b --- /dev/null +++ b/catalog-rest-service/src/main/resources/ui/src/components/schema-editor/SchemaEditor.utils.ts @@ -0,0 +1,24 @@ +import { JSON_TAB_SIZE } from '../../constants/constants'; +import { getJSONFromString } from '../../utils/StringsUtils'; + +export const getSchemaEditorValue = ( + value: string, + autoFormat = true +): string => { + if (typeof value === 'string') { + if (autoFormat) { + const parsedJson = getJSONFromString(value); + + return parsedJson + ? JSON.stringify(parsedJson, null, JSON_TAB_SIZE) + : value; + } else { + return value; + } + } + if (typeof value === 'object') { + return JSON.stringify(value, null, JSON_TAB_SIZE); + } + + return ''; +}; diff --git a/catalog-rest-service/src/main/resources/ui/src/components/tags-container/tags-container.interface.ts b/catalog-rest-service/src/main/resources/ui/src/components/tags-container/tags-container.interface.ts index 7a195955105..bb00f311fc8 100644 --- a/catalog-rest-service/src/main/resources/ui/src/components/tags-container/tags-container.interface.ts +++ b/catalog-rest-service/src/main/resources/ui/src/components/tags-container/tags-container.interface.ts @@ -23,6 +23,7 @@ export type TagsContainerProps = { editable?: boolean; selectedTags: Array; tagList: Array; + showTags?: boolean; onSelectionChange: (selectedTags: Array) => void; onCancel: (event: React.MouseEvent) => void; }; diff --git a/catalog-rest-service/src/main/resources/ui/src/components/tags-container/tags-container.tsx b/catalog-rest-service/src/main/resources/ui/src/components/tags-container/tags-container.tsx index 0e66a018cc9..202cb388c13 100644 --- a/catalog-rest-service/src/main/resources/ui/src/components/tags-container/tags-container.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/components/tags-container/tags-container.tsx @@ -36,6 +36,7 @@ const TagsContainer: FunctionComponent = ({ tagList, onCancel, onSelectionChange, + showTags = true, }: TagsContainerProps) => { const [tags, setTags] = useState>(selectedTags); const [newTag, setNewTag] = useState(''); @@ -124,6 +125,7 @@ const TagsContainer: FunctionComponent = ({ { handleTagRemoval(removedTag, index); @@ -169,7 +171,9 @@ const TagsContainer: FunctionComponent = ({ } }}>
- {tags.map((tag, index) => getTagsElement(tag, index))} + {(showTags || editable) && ( + <>{tags.map((tag, index) => getTagsElement(tag, index))} + )} {editable ? ( , removedTag: string diff --git a/catalog-rest-service/src/main/resources/ui/src/components/tags/tags.tsx b/catalog-rest-service/src/main/resources/ui/src/components/tags/tags.tsx index f5d0ee4c0f3..cc50eee3a14 100644 --- a/catalog-rest-service/src/main/resources/ui/src/components/tags/tags.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/components/tags/tags.tsx @@ -26,6 +26,7 @@ const Tags: FunctionComponent = ({ tag, type = 'contained', removeTag, + isRemovable = true, }: TagProps) => { const baseStyle = tagStyles.base; const layoutStyles = tagStyles[type]; @@ -37,7 +38,7 @@ const Tags: FunctionComponent = ({ return ( {tag} - {editable && ( + {editable && isRemovable && ( ) => { diff --git a/catalog-rest-service/src/main/resources/ui/src/constants/constants.ts b/catalog-rest-service/src/main/resources/ui/src/constants/constants.ts index 00cf422be18..8376acbd90d 100644 --- a/catalog-rest-service/src/main/resources/ui/src/constants/constants.ts +++ b/catalog-rest-service/src/main/resources/ui/src/constants/constants.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +export const JSON_TAB_SIZE = 2; export const PAGE_SIZE = 10; export const API_RES_MAX_SIZE = 100000; export const LIST_SIZE = 5; diff --git a/catalog-rest-service/src/main/resources/ui/src/interface/types.d.ts b/catalog-rest-service/src/main/resources/ui/src/interface/types.d.ts index 03cd2f30f6c..ebe98a3b891 100644 --- a/catalog-rest-service/src/main/resources/ui/src/interface/types.d.ts +++ b/catalog-rest-service/src/main/resources/ui/src/interface/types.d.ts @@ -75,6 +75,7 @@ declare module 'Models' { tagFQN: string; labelType?: 'Manual' | 'Propagated' | 'Automated' | 'Derived'; state?: 'Suggested' | 'Confirmed'; + isRemovable?: boolean; }; export type TableColumn = { @@ -323,4 +324,51 @@ declare module 'Models' { columns: Array; rows: Array>; }; + + // topic interface start + export interface Topic { + cleanupPolicies: string[]; + description: string; + followers: Follower[]; + fullyQualifiedName: string; + href: string; + id: string; + maximumMessageSize: number; + minimumInSyncReplicas: number; + name: string; + owner: Owner; + partitions: number; + retentionSize: number; + retentionTime: number; + schemaText: string; + schemaType: string; + service: Service; + tags: ColumnTags[]; + } + + export interface Follower { + description: string; + href: string; + id: string; + name: string; + type: string; + } + + export interface Owner { + description: string; + href: string; + id: string; + name: string; + type: string; + } + + export interface Service { + description: string; + href: string; + id: string; + name: string; + type: string; + } + + // topic interface end } diff --git a/catalog-rest-service/src/main/resources/ui/src/pages/my-data-details/index.tsx b/catalog-rest-service/src/main/resources/ui/src/pages/my-data-details/index.tsx index 2af56ede0de..264e3f320fa 100644 --- a/catalog-rest-service/src/main/resources/ui/src/pages/my-data-details/index.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/pages/my-data-details/index.tsx @@ -213,7 +213,7 @@ const MyDataDetailsPage = () => { saveUpdatedTableData(updatedTableDetails) .then((res) => { setTableDetails(res.data); - setOwner(res.data.owner); + setOwner(getOwnerFromId(res.data.owner?.id)); setTier(getTierFromTableTags(res.data.tags)); resolve(); }) diff --git a/catalog-rest-service/src/main/resources/ui/src/pages/tags/index.tsx b/catalog-rest-service/src/main/resources/ui/src/pages/tags/index.tsx index 66a52817213..850d27ef8e8 100644 --- a/catalog-rest-service/src/main/resources/ui/src/pages/tags/index.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/pages/tags/index.tsx @@ -377,7 +377,7 @@ const TagsPage = () => { diff --git a/catalog-rest-service/src/main/resources/ui/src/pages/topic-details/index.tsx b/catalog-rest-service/src/main/resources/ui/src/pages/topic-details/index.tsx new file mode 100644 index 00000000000..132586af7ce --- /dev/null +++ b/catalog-rest-service/src/main/resources/ui/src/pages/topic-details/index.tsx @@ -0,0 +1,332 @@ +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 { 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/my-data-details/ManageTab'; +import SchemaEditor from '../../components/schema-editor/SchemaEditor'; +import { getServiceDetailsPath } from '../../constants/constants'; +import { getCurrentUserId, getUserTeams } from '../../utils/CommonUtils'; +import { serviceTypeLogo } from '../../utils/ServiceUtils'; +import { + getOwnerFromId, + getTagsWithoutTier, + getTierFromTableTags, +} from '../../utils/TableUtils'; +import { getTagCategories, getTaglist } from '../../utils/TagsUtils'; + +const MyTopicDetailPage = () => { + const USERId = getCurrentUserId(); + const [tagList, setTagList] = useState>([]); + const { topicFQN } = useParams() as Record; + const [topicDetails, setTopicDetails] = useState({} as Topic); + const [topicId, setTopicId] = useState(''); + const [isLoading, setLoading] = useState(false); + const [description, setDescription] = useState(''); + const [followers, setFollowers] = useState(0); + const [isFollowing, setIsFollowing] = useState(false); + const [owner, setOwner] = useState(); + const [tier, setTier] = useState(); + const [schemaType, setSchemaType] = useState(''); + const [tags, setTags] = useState>([]); + const [activeTab, setActiveTab] = useState(1); + const [partitions, setPartitions] = useState(0); + const [isEdit, setIsEdit] = useState(false); + const [schemaText, setSchemaText] = useState('{}'); + 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: 'Manage', + icon: { + alt: 'manage', + name: 'icon-manage', + title: 'Manage', + }, + isProtected: true, + protectedState: !owner || hasEditAccess(), + position: 2, + }, + ]; + const fetchTags = () => { + getTagCategories().then((res) => { + if (res.data) { + setTagList(getTaglist(res.data)); + } + }); + }; + const fetchTopicDetail = (topicFQN: string) => { + setLoading(true); + getTopicByFqn(topicFQN, ['owner', 'service', 'followers', 'tags']).then( + (res: AxiosResponse) => { + const { + id, + description, + followers, + name, + partitions, + schemaType, + schemaText, + service, + tags, + owner, + } = res.data; + setTopicDetails(res.data); + setTopicId(id); + setDescription(description ?? ''); + setSchemaType(schemaType); + setPartitions(partitions); + setFollowers(followers?.length); + setOwner(getOwnerFromId(owner?.id)); + setTier(getTierFromTableTags(tags)); + setTags(getTagsWithoutTier(tags)); + setSchemaText(schemaText); + setIsFollowing( + followers.some(({ id }: { id: string }) => id === USERId) + ); + 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, + }, + ]); + } + ); + setLoading(false); + } + ); + }; + + const followTopic = (): void => { + if (isFollowing) { + removeFollower(topicId, USERId).then(() => { + setFollowers((preValu) => preValu - 1); + setIsFollowing(false); + }); + } else { + addFollower(topicId, USERId).then(() => { + setFollowers((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 => { + return new Promise((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) => { + if (selectedTags) { + const prevTags = topicDetails.tags.filter((tag) => + selectedTags.includes(tag.tagFQN) + ); + const newTags: Array = 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>) => { + return ( +
+
+ {infos.map((info, index) => ( +
+ + {info.key} + + + {info.value} + +
+ ))} +
+
+
+ ); + }; + + useEffect(() => { + fetchTopicDetail(topicFQN); + }, [topicFQN]); + + useEffect(() => { + fetchTags(); + }, []); + + return ( + + {isLoading ? ( + + ) : ( +
+ +
+ + +
+ {activeTab === 1 && ( + <> +
+
+ +
+
+ {getInfoBadge([ + { key: 'Schema', value: schemaType }, + { key: 'Partitions', value: partitions }, + ])} +
+ +
+ + )} + {activeTab === 2 && ( + + )} +
+
+
+ )} +
+ ); +}; + +export default MyTopicDetailPage; diff --git a/catalog-rest-service/src/main/resources/ui/src/router/AuthenticatedAppRouter.tsx b/catalog-rest-service/src/main/resources/ui/src/router/AuthenticatedAppRouter.tsx index b9eff79debf..6688a7f11e0 100644 --- a/catalog-rest-service/src/main/resources/ui/src/router/AuthenticatedAppRouter.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/router/AuthenticatedAppRouter.tsx @@ -36,6 +36,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 UsersPage from '../pages/users'; import WorkflowsPage from '../pages/workflows'; const AuthenticatedAppRouter: FunctionComponent = () => { @@ -62,7 +63,7 @@ const AuthenticatedAppRouter: FunctionComponent = () => { - + diff --git a/catalog-rest-service/src/main/resources/ui/src/styles/x-master.css b/catalog-rest-service/src/main/resources/ui/src/styles/x-master.css index f40ce27c154..e92b622cb23 100644 --- a/catalog-rest-service/src/main/resources/ui/src/styles/x-master.css +++ b/catalog-rest-service/src/main/resources/ui/src/styles/x-master.css @@ -695,4 +695,8 @@ a:focus { .tippy-popper { pointer-events: auto !important; } +.v-align-sub { + vertical-align: sub; +} + /* popover css end */