mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-13 17:32:53 +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",
|
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
|
||||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
|
"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": {
|
"collect-v8-coverage": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
|
||||||
@ -17262,6 +17267,11 @@
|
|||||||
"warning": "^4.0.3"
|
"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": {
|
"react-dom": {
|
||||||
"version": "16.14.0",
|
"version": "16.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz",
|
||||||
|
|||||||
@ -23,6 +23,7 @@
|
|||||||
"bootstrap": "^4.5.2",
|
"bootstrap": "^4.5.2",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"builtin-status-codes": "^3.0.0",
|
"builtin-status-codes": "^3.0.0",
|
||||||
|
"codemirror": "^5.62.3",
|
||||||
"cookie-storage": "^6.1.0",
|
"cookie-storage": "^6.1.0",
|
||||||
"core-js": "^3.10.1",
|
"core-js": "^3.10.1",
|
||||||
"draft-js": "^0.11.7",
|
"draft-js": "^0.11.7",
|
||||||
@ -53,6 +54,7 @@
|
|||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"react": "^16.14.0",
|
"react": "^16.14.0",
|
||||||
"react-bootstrap": "^1.6.0",
|
"react-bootstrap": "^1.6.0",
|
||||||
|
"react-codemirror2": "^7.2.1",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^16.14.0",
|
||||||
"react-draft-wysiwyg": "^1.14.7",
|
"react-draft-wysiwyg": "^1.14.7",
|
||||||
"react-js-pagination": "^3.0.3",
|
"react-js-pagination": "^3.0.3",
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
|
import { Topic } from 'Models';
|
||||||
import { getURLWithQueryFields } from '../utils/APIUtils';
|
import { getURLWithQueryFields } from '../utils/APIUtils';
|
||||||
import APIClient from './index';
|
import APIClient from './index';
|
||||||
|
|
||||||
@ -31,3 +32,48 @@ export const getTopics: Function = (
|
|||||||
|
|
||||||
return APIClient.get(url);
|
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 ? (
|
tab.isProtected ? (
|
||||||
<NonAdminAction
|
<NonAdminAction
|
||||||
isOwner={tab.protectedState}
|
isOwner={tab.protectedState}
|
||||||
|
key={tab.position}
|
||||||
title="You need to be owner to perform this action">
|
title="You need to be owner to perform this action">
|
||||||
<button
|
<button
|
||||||
className={getTabClasses(tab.position, activeTab)}
|
className={getTabClasses(tab.position, activeTab)}
|
||||||
@ -46,6 +47,7 @@ const TabsPane = ({ activeTab, setActiveTab, tabs }: Props) => {
|
|||||||
<button
|
<button
|
||||||
className={getTabClasses(tab.position, activeTab)}
|
className={getTabClasses(tab.position, activeTab)}
|
||||||
data-testid="tab"
|
data-testid="tab"
|
||||||
|
key={tab.position}
|
||||||
onClick={() => setActiveTab(tab.position)}>
|
onClick={() => setActiveTab(tab.position)}>
|
||||||
<SVGIcons
|
<SVGIcons
|
||||||
alt={tab.icon.alt}
|
alt={tab.icon.alt}
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { ColumnTags } from 'Models';
|
import { ColumnTags } from 'Models';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { LIST_SIZE } from '../../../constants/constants';
|
import { LIST_SIZE } from '../../../constants/constants';
|
||||||
|
import SVGIcons from '../../../utils/SvgUtils';
|
||||||
|
import TagsContainer from '../../tags-container/tags-container';
|
||||||
import Tags from '../../tags/tags';
|
import Tags from '../../tags/tags';
|
||||||
import PopOver from '../popover/PopOver';
|
import PopOver from '../popover/PopOver';
|
||||||
import TitleBreadcrumb from '../title-breadcrumb/title-breadcrumb.component';
|
import TitleBreadcrumb from '../title-breadcrumb/title-breadcrumb.component';
|
||||||
@ -20,6 +22,9 @@ type Props = {
|
|||||||
extraInfo: Array<ExtraInfo>;
|
extraInfo: Array<ExtraInfo>;
|
||||||
tier: string;
|
tier: string;
|
||||||
tags: Array<ColumnTags>;
|
tags: Array<ColumnTags>;
|
||||||
|
isTagEditable?: boolean;
|
||||||
|
tagList?: Array<string>;
|
||||||
|
tagsHandler?: (selectedTags?: Array<string>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EntityPageInfo = ({
|
const EntityPageInfo = ({
|
||||||
@ -30,7 +35,17 @@ const EntityPageInfo = ({
|
|||||||
extraInfo,
|
extraInfo,
|
||||||
tier,
|
tier,
|
||||||
tags,
|
tags,
|
||||||
|
isTagEditable = false,
|
||||||
|
tagList = [],
|
||||||
|
tagsHandler,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const [isEditable, setIsEditable] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleTagSelection = (selectedTags?: Array<ColumnTags>) => {
|
||||||
|
tagsHandler?.(selectedTags?.map((tag) => tag.tagFQN));
|
||||||
|
setIsEditable(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="tw-flex tw-flex-col">
|
<div className="tw-flex tw-flex-col">
|
||||||
@ -81,47 +96,90 @@ const EntityPageInfo = ({
|
|||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="tw-flex tw-flex-wrap tw-pt-1">
|
<div className="tw-flex tw-flex-wrap tw-pt-1 tw-group">
|
||||||
{(tags.length > 0 || tier) && (
|
{(!isEditable || !isTagEditable) && (
|
||||||
<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 && (
|
|
||||||
<>
|
<>
|
||||||
{tags.slice(0, LIST_SIZE).map((tag, index) => (
|
{(tags.length > 0 || tier) && (
|
||||||
<Tags
|
<i className="fas fa-tags tw-px-1 tw-mt-2 tw-text-grey-muted" />
|
||||||
className="tw-bg-gray-200"
|
)}
|
||||||
key={index}
|
{tier && (
|
||||||
tag={`#${tag.tagFQN}`}
|
<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 && (
|
{tags.slice(LIST_SIZE).length > 0 && (
|
||||||
<PopOver
|
<PopOver
|
||||||
className="tw-py-1"
|
html={
|
||||||
html={
|
<>
|
||||||
<>
|
{tags.slice(LIST_SIZE).map((tag, index) => (
|
||||||
{tags.slice(LIST_SIZE).map((tag, index) => (
|
<Tags
|
||||||
<Tags
|
className="tw-bg-tag tw-px-2"
|
||||||
className="tw-bg-gray-200 tw-px-2"
|
key={index}
|
||||||
key={index}
|
tag={`#${tag.tagFQN}`}
|
||||||
tag={`#${tag.tagFQN}`}
|
/>
|
||||||
/>
|
))}
|
||||||
))}
|
</>
|
||||||
</>
|
}
|
||||||
}
|
position="bottom"
|
||||||
position="bottom"
|
theme="light"
|
||||||
theme="light"
|
trigger="click">
|
||||||
trigger="click">
|
<span className="tw-cursor-pointer tw-text-xs link-text v-align-sub tw--ml-1">
|
||||||
<span className="tw-cursor-pointer tw-text-xs link-text">
|
•••
|
||||||
View more
|
</span>
|
||||||
</span>
|
</PopOver>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -287,7 +287,7 @@ const SchemaTable: FunctionComponent<Props> = ({
|
|||||||
<SVGIcons
|
<SVGIcons
|
||||||
alt="edit"
|
alt="edit"
|
||||||
icon="icon-edit"
|
icon="icon-edit"
|
||||||
title="edit"
|
title="Edit"
|
||||||
width="10px"
|
width="10px"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
@ -386,7 +386,7 @@ const SchemaTable: FunctionComponent<Props> = ({
|
|||||||
<SVGIcons
|
<SVGIcons
|
||||||
alt="edit"
|
alt="edit"
|
||||||
icon="icon-edit"
|
icon="icon-edit"
|
||||||
title="edit"
|
title="Edit"
|
||||||
width="10px"
|
width="10px"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
@ -394,7 +394,7 @@ const SchemaTable: FunctionComponent<Props> = ({
|
|||||||
<span className="tw-opacity-0 group-hover:tw-opacity-100">
|
<span className="tw-opacity-0 group-hover:tw-opacity-100">
|
||||||
<Tags
|
<Tags
|
||||||
className="tw-border-main"
|
className="tw-border-main"
|
||||||
tag="+ Add new tag"
|
tag="+ Add tag"
|
||||||
type="outlined"
|
type="outlined"
|
||||||
/>
|
/>
|
||||||
</span>
|
</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;
|
editable?: boolean;
|
||||||
selectedTags: Array<ColumnTags>;
|
selectedTags: Array<ColumnTags>;
|
||||||
tagList: Array<string>;
|
tagList: Array<string>;
|
||||||
|
showTags?: boolean;
|
||||||
onSelectionChange: (selectedTags: Array<ColumnTags>) => void;
|
onSelectionChange: (selectedTags: Array<ColumnTags>) => void;
|
||||||
onCancel: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
onCancel: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -36,6 +36,7 @@ const TagsContainer: FunctionComponent<TagsContainerProps> = ({
|
|||||||
tagList,
|
tagList,
|
||||||
onCancel,
|
onCancel,
|
||||||
onSelectionChange,
|
onSelectionChange,
|
||||||
|
showTags = true,
|
||||||
}: TagsContainerProps) => {
|
}: TagsContainerProps) => {
|
||||||
const [tags, setTags] = useState<Array<ColumnTags>>(selectedTags);
|
const [tags, setTags] = useState<Array<ColumnTags>>(selectedTags);
|
||||||
const [newTag, setNewTag] = useState<string>('');
|
const [newTag, setNewTag] = useState<string>('');
|
||||||
@ -124,6 +125,7 @@ const TagsContainer: FunctionComponent<TagsContainerProps> = ({
|
|||||||
<Tags
|
<Tags
|
||||||
className="tw-bg-gray-200"
|
className="tw-bg-gray-200"
|
||||||
editable={editable}
|
editable={editable}
|
||||||
|
isRemovable={tag.isRemovable}
|
||||||
key={index}
|
key={index}
|
||||||
removeTag={(_e, removedTag: string) => {
|
removeTag={(_e, removedTag: string) => {
|
||||||
handleTagRemoval(removedTag, index);
|
handleTagRemoval(removedTag, index);
|
||||||
@ -169,7 +171,9 @@ const TagsContainer: FunctionComponent<TagsContainerProps> = ({
|
|||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<div className="tw-flex tw-flex-wrap">
|
<div className="tw-flex tw-flex-wrap">
|
||||||
{tags.map((tag, index) => getTagsElement(tag, index))}
|
{(showTags || editable) && (
|
||||||
|
<>{tags.map((tag, index) => getTagsElement(tag, index))}</>
|
||||||
|
)}
|
||||||
{editable ? (
|
{editable ? (
|
||||||
<span className="tw-relative">
|
<span className="tw-relative">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@ -20,6 +20,7 @@ export type TagProps = {
|
|||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
tag: string;
|
tag: string;
|
||||||
type?: 'contained' | 'outlined';
|
type?: 'contained' | 'outlined';
|
||||||
|
isRemovable?: boolean;
|
||||||
removeTag?: (
|
removeTag?: (
|
||||||
event: React.MouseEvent<HTMLElement, MouseEvent>,
|
event: React.MouseEvent<HTMLElement, MouseEvent>,
|
||||||
removedTag: string
|
removedTag: string
|
||||||
|
|||||||
@ -26,6 +26,7 @@ const Tags: FunctionComponent<TagProps> = ({
|
|||||||
tag,
|
tag,
|
||||||
type = 'contained',
|
type = 'contained',
|
||||||
removeTag,
|
removeTag,
|
||||||
|
isRemovable = true,
|
||||||
}: TagProps) => {
|
}: TagProps) => {
|
||||||
const baseStyle = tagStyles.base;
|
const baseStyle = tagStyles.base;
|
||||||
const layoutStyles = tagStyles[type];
|
const layoutStyles = tagStyles[type];
|
||||||
@ -37,7 +38,7 @@ const Tags: FunctionComponent<TagProps> = ({
|
|||||||
return (
|
return (
|
||||||
<span className={classNames(baseStyle, layoutStyles, className)}>
|
<span className={classNames(baseStyle, layoutStyles, className)}>
|
||||||
<span className={classNames(textBaseStyle, textLayoutStyles)}>{tag}</span>
|
<span className={classNames(textBaseStyle, textLayoutStyles)}>{tag}</span>
|
||||||
{editable && (
|
{editable && isRemovable && (
|
||||||
<span
|
<span
|
||||||
className="tw-py-1 tw-px-2 tw-rounded tw-cursor-pointer"
|
className="tw-py-1 tw-px-2 tw-rounded tw-cursor-pointer"
|
||||||
onClick={(e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
onClick={(e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export const JSON_TAB_SIZE = 2;
|
||||||
export const PAGE_SIZE = 10;
|
export const PAGE_SIZE = 10;
|
||||||
export const API_RES_MAX_SIZE = 100000;
|
export const API_RES_MAX_SIZE = 100000;
|
||||||
export const LIST_SIZE = 5;
|
export const LIST_SIZE = 5;
|
||||||
|
|||||||
@ -75,6 +75,7 @@ declare module 'Models' {
|
|||||||
tagFQN: string;
|
tagFQN: string;
|
||||||
labelType?: 'Manual' | 'Propagated' | 'Automated' | 'Derived';
|
labelType?: 'Manual' | 'Propagated' | 'Automated' | 'Derived';
|
||||||
state?: 'Suggested' | 'Confirmed';
|
state?: 'Suggested' | 'Confirmed';
|
||||||
|
isRemovable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TableColumn = {
|
export type TableColumn = {
|
||||||
@ -323,4 +324,51 @@ declare module 'Models' {
|
|||||||
columns: Array<string>;
|
columns: Array<string>;
|
||||||
rows: Array<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)
|
saveUpdatedTableData(updatedTableDetails)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setTableDetails(res.data);
|
setTableDetails(res.data);
|
||||||
setOwner(res.data.owner);
|
setOwner(getOwnerFromId(res.data.owner?.id));
|
||||||
setTier(getTierFromTableTags(res.data.tags));
|
setTier(getTierFromTableTags(res.data.tags));
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
|
|||||||
@ -377,7 +377,7 @@ const TagsPage = () => {
|
|||||||
<span className="tw-opacity-0 group-hover:tw-opacity-100">
|
<span className="tw-opacity-0 group-hover:tw-opacity-100">
|
||||||
<Tags
|
<Tags
|
||||||
className="tw-border-main"
|
className="tw-border-main"
|
||||||
tag="+ Add new tag"
|
tag="+ Add tag"
|
||||||
type="outlined"
|
type="outlined"
|
||||||
/>
|
/>
|
||||||
</span>
|
</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 SwaggerPage from '../pages/swagger';
|
||||||
import TagsPage from '../pages/tags';
|
import TagsPage from '../pages/tags';
|
||||||
import TeamsPage from '../pages/teams';
|
import TeamsPage from '../pages/teams';
|
||||||
|
import MyTopicDetailPage from '../pages/topic-details';
|
||||||
import UsersPage from '../pages/users';
|
import UsersPage from '../pages/users';
|
||||||
import WorkflowsPage from '../pages/workflows';
|
import WorkflowsPage from '../pages/workflows';
|
||||||
const AuthenticatedAppRouter: FunctionComponent = () => {
|
const AuthenticatedAppRouter: FunctionComponent = () => {
|
||||||
@ -62,7 +63,7 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
|
|||||||
<Route exact component={TagsPage} path={ROUTES.TAGS} />
|
<Route exact component={TagsPage} path={ROUTES.TAGS} />
|
||||||
<Route component={DatabaseDetails} path={ROUTES.DATABASE_DETAILS} />
|
<Route component={DatabaseDetails} path={ROUTES.DATABASE_DETAILS} />
|
||||||
<Route component={MyDataDetailsPage} path={ROUTES.DATASET_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} />
|
<Route component={Onboarding} path={ROUTES.ONBOARDING} />
|
||||||
<Redirect to={ROUTES.NOT_FOUND} />
|
<Redirect to={ROUTES.NOT_FOUND} />
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|||||||
@ -695,4 +695,8 @@ a:focus {
|
|||||||
.tippy-popper {
|
.tippy-popper {
|
||||||
pointer-events: auto !important;
|
pointer-events: auto !important;
|
||||||
}
|
}
|
||||||
|
.v-align-sub {
|
||||||
|
vertical-align: sub;
|
||||||
|
}
|
||||||
|
|
||||||
/* popover css end */
|
/* popover css end */
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user