mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-14 01:40:08 +00:00
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:
parent
eff1de87ef
commit
4d3ec274ea
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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; */
|
||||
}
|
||||
@ -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;
|
||||
@ -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}
|
||||
|
||||
@ -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,18 +96,20 @@ const EntityPageInfo = ({
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="tw-flex tw-flex-wrap tw-pt-1">
|
||||
<div className="tw-flex tw-flex-wrap tw-pt-1 tw-group">
|
||||
{(!isEditable || !isTagEditable) && (
|
||||
<>
|
||||
{(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 className="tw-bg-tag" tag={`#${tier.split('.')[1]}`} />
|
||||
)}
|
||||
{tags.length > 0 && (
|
||||
<>
|
||||
{tags.slice(0, LIST_SIZE).map((tag, index) => (
|
||||
<Tags
|
||||
className="tw-bg-gray-200"
|
||||
className="tw-bg-tag"
|
||||
key={index}
|
||||
tag={`#${tag.tagFQN}`}
|
||||
/>
|
||||
@ -100,12 +117,11 @@ const EntityPageInfo = ({
|
||||
|
||||
{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"
|
||||
className="tw-bg-tag tw-px-2"
|
||||
key={index}
|
||||
tag={`#${tag.tagFQN}`}
|
||||
/>
|
||||
@ -115,13 +131,55 @@ const EntityPageInfo = ({
|
||||
position="bottom"
|
||||
theme="light"
|
||||
trigger="click">
|
||||
<span className="tw-cursor-pointer tw-text-xs link-text">
|
||||
View more
|
||||
<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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
@ -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 '';
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -20,6 +20,7 @@ export type TagProps = {
|
||||
editable?: boolean;
|
||||
tag: string;
|
||||
type?: 'contained' | 'outlined';
|
||||
isRemovable?: boolean;
|
||||
removeTag?: (
|
||||
event: React.MouseEvent<HTMLElement, MouseEvent>,
|
||||
removedTag: string
|
||||
|
||||
@ -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>) => {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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();
|
||||
})
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
@ -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>
|
||||
|
||||
@ -695,4 +695,8 @@ a:focus {
|
||||
.tippy-popper {
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
.v-align-sub {
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
/* popover css end */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user