mirror of
				https://github.com/open-metadata/OpenMetadata.git
				synced 2025-11-03 20:19:31 +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,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>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
@ -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