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
This commit is contained in:
Sachin Chaurasiya 2021-09-03 09:31:57 +05:30 committed by GitHub
parent eff1de87ef
commit 4d3ec274ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 790 additions and 45 deletions

View File

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

View File

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

View File

@ -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<AxiosResponse> => {
const url = getURLWithQueryFields(`/topics/name/${fqn}`, arrQueryFields);
return APIClient.get(url);
};
export const addFollower: Function = (
topicId: string,
userId: string
): Promise<AxiosResponse> => {
const configOptions = {
headers: { 'Content-type': 'application/json' },
};
return APIClient.put(`/topics/${topicId}/followers`, userId, configOptions);
};
export const removeFollower: Function = (
topicId: string,
userId: string
): Promise<AxiosResponse> => {
const configOptions = {
headers: { 'Content-type': 'application/json' },
};
return APIClient.delete(
`/topics/${topicId}/followers/${userId}`,
configOptions
);
};
export const patchTopicDetails: Function = (
id: string,
data: Topic
): Promise<AxiosResponse> => {
const configOptions = {
headers: { 'Content-type': 'application/json-patch+json' },
};
return APIClient.patch(`/topics/${id}`, data, configOptions);
};

View File

@ -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; */
}

View File

@ -0,0 +1,107 @@
import React, { CSSProperties, useCallback, useState } from 'react';
import './SchemaTreeStructure.css';
type Props = {
positions?: Array<number>;
name: string;
type: string;
fields?: Array<Props>;
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<boolean>(!isCollapsed);
const flag = (fields ?? []).length > 0;
const showChildrenHandler = useCallback(() => {
setShowChildren(!showChildren);
}, [showChildren, setShowChildren]);
const getIcon = () => {
return (
flag &&
(showChildren ? (
<i className="fas fa-minus-circle" />
) : (
<i className="fas fa-plus-circle" />
))
);
};
return (
<div
className="field-wrapper"
style={{ paddingLeft: flag ? '26px' : '0px' }}>
<div
className="field-child"
style={{ marginLeft: flag ? '-26px' : '0px' }}>
<p className="field-child-icon" onClick={showChildrenHandler}>
{getIcon()}
</p>
<p className="field-label">
<span style={getStyle(type) as CSSProperties}>{type}</span>
<span className="field-label-name">{name}</span>
</p>
</div>
{flag && showChildren && (
<div className="child-fields-wrapper">
{(fields ?? []).map((field, index) => (
<SchemaTreeStructure
isCollapsed
key={index}
positions={[...positions, index]}
{...field}
/>
))}
</div>
)}
</div>
);
};
export default SchemaTreeStructure;

View File

@ -29,6 +29,7 @@ const TabsPane = ({ activeTab, setActiveTab, tabs }: Props) => {
tab.isProtected ? (
<NonAdminAction
isOwner={tab.protectedState}
key={tab.position}
title="You need to be owner to perform this action">
<button
className={getTabClasses(tab.position, activeTab)}
@ -46,6 +47,7 @@ const TabsPane = ({ activeTab, setActiveTab, tabs }: Props) => {
<button
className={getTabClasses(tab.position, activeTab)}
data-testid="tab"
key={tab.position}
onClick={() => setActiveTab(tab.position)}>
<SVGIcons
alt={tab.icon.alt}

View File

@ -1,7 +1,9 @@
import classNames from 'classnames';
import { ColumnTags } from 'Models';
import React from 'react';
import React, { useState } from 'react';
import { LIST_SIZE } from '../../../constants/constants';
import SVGIcons from '../../../utils/SvgUtils';
import TagsContainer from '../../tags-container/tags-container';
import Tags from '../../tags/tags';
import PopOver from '../popover/PopOver';
import TitleBreadcrumb from '../title-breadcrumb/title-breadcrumb.component';
@ -20,6 +22,9 @@ type Props = {
extraInfo: Array<ExtraInfo>;
tier: string;
tags: Array<ColumnTags>;
isTagEditable?: boolean;
tagList?: Array<string>;
tagsHandler?: (selectedTags?: Array<string>) => void;
};
const EntityPageInfo = ({
@ -30,7 +35,17 @@ const EntityPageInfo = ({
extraInfo,
tier,
tags,
isTagEditable = false,
tagList = [],
tagsHandler,
}: Props) => {
const [isEditable, setIsEditable] = useState<boolean>(false);
const handleTagSelection = (selectedTags?: Array<ColumnTags>) => {
tagsHandler?.(selectedTags?.map((tag) => tag.tagFQN));
setIsEditable(false);
};
return (
<div>
<div className="tw-flex tw-flex-col">
@ -81,47 +96,90 @@ const EntityPageInfo = ({
</span>
))}
</div>
<div className="tw-flex tw-flex-wrap tw-pt-1">
{(tags.length > 0 || tier) && (
<i className="fas fa-tags tw-px-1 tw-mt-2 tw-text-grey-muted" />
)}
{tier && (
<Tags className="tw-bg-gray-200" tag={`#${tier.split('.')[1]}`} />
)}
{tags.length > 0 && (
<div className="tw-flex tw-flex-wrap tw-pt-1 tw-group">
{(!isEditable || !isTagEditable) && (
<>
{tags.slice(0, LIST_SIZE).map((tag, index) => (
<Tags
className="tw-bg-gray-200"
key={index}
tag={`#${tag.tagFQN}`}
/>
))}
{(tags.length > 0 || tier) && (
<i className="fas fa-tags tw-px-1 tw-mt-2 tw-text-grey-muted" />
)}
{tier && (
<Tags className="tw-bg-tag" tag={`#${tier.split('.')[1]}`} />
)}
{tags.length > 0 && (
<>
{tags.slice(0, LIST_SIZE).map((tag, index) => (
<Tags
className="tw-bg-tag"
key={index}
tag={`#${tag.tagFQN}`}
/>
))}
{tags.slice(LIST_SIZE).length > 0 && (
<PopOver
className="tw-py-1"
html={
<>
{tags.slice(LIST_SIZE).map((tag, index) => (
<Tags
className="tw-bg-gray-200 tw-px-2"
key={index}
tag={`#${tag.tagFQN}`}
/>
))}
</>
}
position="bottom"
theme="light"
trigger="click">
<span className="tw-cursor-pointer tw-text-xs link-text">
View more
</span>
</PopOver>
{tags.slice(LIST_SIZE).length > 0 && (
<PopOver
html={
<>
{tags.slice(LIST_SIZE).map((tag, index) => (
<Tags
className="tw-bg-tag tw-px-2"
key={index}
tag={`#${tag.tagFQN}`}
/>
))}
</>
}
position="bottom"
theme="light"
trigger="click">
<span className="tw-cursor-pointer tw-text-xs link-text v-align-sub tw--ml-1">
</span>
</PopOver>
)}
</>
)}
</>
)}
{isTagEditable && (
<div onClick={() => setIsEditable(true)}>
<TagsContainer
editable={isEditable}
selectedTags={[
...tags.map((tag) => ({
tagFQN: tag.tagFQN,
isRemovable: true,
})),
{ tagFQN: tier, isRemovable: false },
]}
showTags={!isTagEditable}
tagList={tagList}
onCancel={() => {
handleTagSelection();
}}
onSelectionChange={(tags) => {
handleTagSelection(tags);
}}>
{tags.length || tier ? (
<button className=" tw-ml-1 focus:tw-outline-none">
<SVGIcons
alt="edit"
icon="icon-edit"
title="Edit"
width="12px"
/>
</button>
) : (
<span className="">
<Tags
className="tw-border-main tw-text-primary"
tag="+ Add tag"
type="outlined"
/>
</span>
)}
</TagsContainer>
</div>
)}
</div>
</div>
);

View File

@ -287,7 +287,7 @@ const SchemaTable: FunctionComponent<Props> = ({
<SVGIcons
alt="edit"
icon="icon-edit"
title="edit"
title="Edit"
width="10px"
/>
</button>
@ -386,7 +386,7 @@ const SchemaTable: FunctionComponent<Props> = ({
<SVGIcons
alt="edit"
icon="icon-edit"
title="edit"
title="Edit"
width="10px"
/>
</button>
@ -394,7 +394,7 @@ const SchemaTable: FunctionComponent<Props> = ({
<span className="tw-opacity-0 group-hover:tw-opacity-100">
<Tags
className="tw-border-main"
tag="+ Add new tag"
tag="+ Add tag"
type="outlined"
/>
</span>

View File

@ -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 (
<div>
<CodeMirror
options={options}
value={internalValue}
onBeforeChange={handleEditorInputBeforeChange}
/>
</div>
);
};
export default SchemaEditor;

View File

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

View File

@ -23,6 +23,7 @@ export type TagsContainerProps = {
editable?: boolean;
selectedTags: Array<ColumnTags>;
tagList: Array<string>;
showTags?: boolean;
onSelectionChange: (selectedTags: Array<ColumnTags>) => void;
onCancel: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
};

View File

@ -36,6 +36,7 @@ const TagsContainer: FunctionComponent<TagsContainerProps> = ({
tagList,
onCancel,
onSelectionChange,
showTags = true,
}: TagsContainerProps) => {
const [tags, setTags] = useState<Array<ColumnTags>>(selectedTags);
const [newTag, setNewTag] = useState<string>('');
@ -124,6 +125,7 @@ const TagsContainer: FunctionComponent<TagsContainerProps> = ({
<Tags
className="tw-bg-gray-200"
editable={editable}
isRemovable={tag.isRemovable}
key={index}
removeTag={(_e, removedTag: string) => {
handleTagRemoval(removedTag, index);
@ -169,7 +171,9 @@ const TagsContainer: FunctionComponent<TagsContainerProps> = ({
}
}}>
<div className="tw-flex tw-flex-wrap">
{tags.map((tag, index) => getTagsElement(tag, index))}
{(showTags || editable) && (
<>{tags.map((tag, index) => getTagsElement(tag, index))}</>
)}
{editable ? (
<span className="tw-relative">
<input

View File

@ -20,6 +20,7 @@ export type TagProps = {
editable?: boolean;
tag: string;
type?: 'contained' | 'outlined';
isRemovable?: boolean;
removeTag?: (
event: React.MouseEvent<HTMLElement, MouseEvent>,
removedTag: string

View File

@ -26,6 +26,7 @@ const Tags: FunctionComponent<TagProps> = ({
tag,
type = 'contained',
removeTag,
isRemovable = true,
}: TagProps) => {
const baseStyle = tagStyles.base;
const layoutStyles = tagStyles[type];
@ -37,7 +38,7 @@ const Tags: FunctionComponent<TagProps> = ({
return (
<span className={classNames(baseStyle, layoutStyles, className)}>
<span className={classNames(textBaseStyle, textLayoutStyles)}>{tag}</span>
{editable && (
{editable && isRemovable && (
<span
className="tw-py-1 tw-px-2 tw-rounded tw-cursor-pointer"
onClick={(e: React.MouseEvent<HTMLElement, MouseEvent>) => {

View File

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

View File

@ -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<string>;
rows: Array<Array<string>>;
};
// 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
}

View File

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

View File

@ -377,7 +377,7 @@ const TagsPage = () => {
<span className="tw-opacity-0 group-hover:tw-opacity-100">
<Tags
className="tw-border-main"
tag="+ Add new tag"
tag="+ Add tag"
type="outlined"
/>
</span>

View File

@ -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<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<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 [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: '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<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>
);
};
useEffect(() => {
fetchTopicDetail(topicFQN);
}, [topicFQN]);
useEffect(() => {
fetchTags();
}, []);
return (
<PageContainer>
{isLoading ? (
<Loader />
) : (
<div className="tw-px-4 w-full">
<EntityPageInfo
isTagEditable
extraInfo={[
{ key: 'Owner', value: owner?.name || '' },
{ key: 'Tier', value: tier ? tier.split('.')[1] : '' },
]}
followers={followers}
followHandler={followTopic}
isFollowing={isFollowing}
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">
{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 },
{ key: 'Partitions', value: partitions },
])}
<div className="tw-my-4 tw-border tw-border-main tw-rounded-md tw-py-4">
<SchemaEditor value={schemaText} />
</div>
</>
)}
{activeTab === 2 && (
<ManageTab
currentTier={tier}
currentUser={owner?.id}
hasEditAccess={hasEditAccess()}
onSave={onSettingsUpdate}
/>
)}
</div>
</div>
</div>
)}
</PageContainer>
);
};
export default MyTopicDetailPage;

View File

@ -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 = () => {
<Route exact component={TagsPage} path={ROUTES.TAGS} />
<Route component={DatabaseDetails} path={ROUTES.DATABASE_DETAILS} />
<Route component={MyDataDetailsPage} path={ROUTES.DATASET_DETAILS} />
<Route component={MyDataDetailsPage} path={ROUTES.TOPIC_DETAILS} />
<Route component={MyTopicDetailPage} path={ROUTES.TOPIC_DETAILS} />
<Route component={Onboarding} path={ROUTES.ONBOARDING} />
<Redirect to={ROUTES.NOT_FOUND} />
</Switch>

View File

@ -695,4 +695,8 @@ a:focus {
.tippy-popper {
pointer-events: auto !important;
}
.v-align-sub {
vertical-align: sub;
}
/* popover css end */