mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2026-01-08 13:36:32 +00:00
parent
a31620af3d
commit
72ba2a7a5f
@ -18,6 +18,7 @@ import {
|
||||
faCheckSquare,
|
||||
faChevronDown,
|
||||
faChevronRight,
|
||||
faChevronUp,
|
||||
faPlus,
|
||||
faSearch,
|
||||
faTimes,
|
||||
@ -39,7 +40,8 @@ const App: FunctionComponent = () => {
|
||||
faCheckSquare,
|
||||
faCheckCircle,
|
||||
faChevronDown,
|
||||
faChevronRight
|
||||
faChevronRight,
|
||||
faChevronUp
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import SampleDataTopic from './SampleDataTopic';
|
||||
|
||||
const mockSampleData = {
|
||||
messages: ['{"email":"data","name":"job"}'],
|
||||
};
|
||||
|
||||
describe('Test SampleData Component', () => {
|
||||
it('Should render message cards', () => {
|
||||
const { getAllByTestId } = render(
|
||||
<SampleDataTopic sampleData={mockSampleData} />,
|
||||
{
|
||||
wrapper: MemoryRouter,
|
||||
}
|
||||
);
|
||||
|
||||
const messageCards = getAllByTestId('message-card');
|
||||
|
||||
expect(messageCards).toHaveLength(mockSampleData.messages.length);
|
||||
});
|
||||
|
||||
it('Should render no data placeholder if no data available', () => {
|
||||
const { getByTestId } = render(<SampleDataTopic />, {
|
||||
wrapper: MemoryRouter,
|
||||
});
|
||||
|
||||
const noDataPlaceHolder = getByTestId('no-data');
|
||||
|
||||
expect(noDataPlaceHolder).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { isUndefined } from 'lodash';
|
||||
import React, { FC, HTMLAttributes, useState } from 'react';
|
||||
import { TopicSampleData } from '../../generated/entity/data/topic';
|
||||
import { withLoader } from '../../hoc/withLoader';
|
||||
import SchemaEditor from '../schema-editor/SchemaEditor';
|
||||
|
||||
interface SampleDataTopicProp extends HTMLAttributes<HTMLDivElement> {
|
||||
sampleData?: TopicSampleData;
|
||||
}
|
||||
|
||||
const MessageCard = ({ message }: { message: string }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="tw-bg-white tw-shadow tw-rounded tw-p-2 tw-mb-6 tw-border tw-border-main"
|
||||
data-testid="message-card"
|
||||
onClick={() => setIsExpanded((pre) => !pre)}>
|
||||
<div className="tw-flex">
|
||||
<div className="tw-mr-3 tw-cursor-pointer">
|
||||
<FontAwesomeIcon
|
||||
className="tw-text-xs"
|
||||
icon={isExpanded ? 'chevron-up' : 'chevron-down'}
|
||||
/>
|
||||
</div>
|
||||
{isExpanded ? (
|
||||
<div>
|
||||
<button
|
||||
className="tw-gh-tabs active tw--mt-4"
|
||||
data-testid="value"
|
||||
id="sampleData-value">
|
||||
Value
|
||||
</button>
|
||||
<SchemaEditor
|
||||
className="tw-mt-2"
|
||||
editorClass="topic-sample-data"
|
||||
options={{
|
||||
styleActiveLine: false,
|
||||
}}
|
||||
value={message.replace(/'/g, '"')}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<p>
|
||||
<p
|
||||
className="tw-my-1 topic-sample-data-message"
|
||||
style={{ color: '#450de2' }}>
|
||||
{message}
|
||||
</p>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SampleDataTopic: FC<SampleDataTopicProp> = ({ sampleData }) => {
|
||||
if (!isUndefined(sampleData)) {
|
||||
return (
|
||||
<div className="tw-p-4 tw-flex tw-flex-col">
|
||||
{sampleData.messages?.map((message, i) => (
|
||||
<MessageCard key={i} message={message} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
className="tw-flex tw-justify-center tw-font-medium tw-items-center tw-border tw-border-main tw-rounded-md tw-p-8"
|
||||
data-testid="no-data">
|
||||
No sample data available
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default withLoader<SampleDataTopicProp>(SampleDataTopic);
|
||||
@ -12,7 +12,7 @@
|
||||
*/
|
||||
|
||||
import { EntityTags } from 'Models';
|
||||
import React, { RefObject, useEffect, useState } from 'react';
|
||||
import React, { Fragment, RefObject, useEffect, useState } from 'react';
|
||||
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider';
|
||||
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
|
||||
import { getTeamAndUserDetailsPath } from '../../constants/constants';
|
||||
@ -43,6 +43,7 @@ import PageContainer from '../containers/PageContainer';
|
||||
import Loader from '../Loader/Loader';
|
||||
import ManageTabComponent from '../ManageTab/ManageTab.component';
|
||||
import RequestDescriptionModal from '../Modals/RequestDescriptionModal/RequestDescriptionModal';
|
||||
import SampleDataTopic from '../SampleDataTopic/SampleDataTopic';
|
||||
import SchemaEditor from '../schema-editor/SchemaEditor';
|
||||
import { TopicDetailsProps } from './TopicDetails.interface';
|
||||
|
||||
@ -83,6 +84,8 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
|
||||
deletePostHandler,
|
||||
paging,
|
||||
fetchFeedHandler,
|
||||
isSampleDataLoading,
|
||||
sampleData,
|
||||
}: TopicDetailsProps) => {
|
||||
const { isAuthDisabled } = useAuthContext();
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
@ -168,6 +171,17 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
|
||||
position: 2,
|
||||
count: feedCount,
|
||||
},
|
||||
{
|
||||
name: 'Sample Data',
|
||||
icon: {
|
||||
alt: 'sample_data',
|
||||
name: 'sample-data',
|
||||
title: 'Sample Data',
|
||||
selectedName: 'sample-data-color',
|
||||
},
|
||||
isProtected: false,
|
||||
position: 3,
|
||||
},
|
||||
{
|
||||
name: 'Config',
|
||||
icon: {
|
||||
@ -177,7 +191,7 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
|
||||
selectedName: 'icon-configcolor',
|
||||
},
|
||||
isProtected: false,
|
||||
position: 3,
|
||||
position: 4,
|
||||
},
|
||||
{
|
||||
name: 'Manage',
|
||||
@ -190,7 +204,7 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
|
||||
isProtected: true,
|
||||
isHidden: deleted,
|
||||
protectedState: !owner || hasEditAccess(),
|
||||
position: 4,
|
||||
position: 5,
|
||||
},
|
||||
];
|
||||
const extraInfo = [
|
||||
@ -400,12 +414,20 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{getInfoBadge([{ key: 'Schema', value: schemaType }])}
|
||||
<div
|
||||
className="tw-my-4 tw-border tw-border-main tw-rounded-md tw-py-4"
|
||||
data-testid="schema">
|
||||
<SchemaEditor value={schemaText} />
|
||||
</div>
|
||||
{schemaText ? (
|
||||
<Fragment>
|
||||
{getInfoBadge([{ key: 'Schema', value: schemaType }])}
|
||||
<div
|
||||
className="tw-my-4 tw-border tw-border-main tw-rounded-md tw-py-4"
|
||||
data-testid="schema">
|
||||
<SchemaEditor value={schemaText} />
|
||||
</div>
|
||||
</Fragment>
|
||||
) : (
|
||||
<div className="tw-flex tw-justify-center tw-font-medium tw-items-center tw-border tw-border-main tw-rounded-md tw-p-8">
|
||||
No schema data available
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{activeTab === 2 && (
|
||||
@ -426,11 +448,19 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 3 && (
|
||||
<div data-testid="sample-data">
|
||||
<SampleDataTopic
|
||||
isLoading={isSampleDataLoading}
|
||||
sampleData={sampleData}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 4 && (
|
||||
<div data-testid="config">
|
||||
<SchemaEditor value={JSON.stringify(getConfigObject())} />
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 4 && !deleted && (
|
||||
{activeTab === 5 && !deleted && (
|
||||
<div>
|
||||
<ManageTabComponent
|
||||
allowDelete
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
|
||||
import { EntityFieldThreadCount, EntityTags, EntityThread } from 'Models';
|
||||
import { CreateThread } from '../../generated/api/feed/createThread';
|
||||
import { Topic } from '../../generated/entity/data/topic';
|
||||
import { Topic, TopicSampleData } from '../../generated/entity/data/topic';
|
||||
import { User } from '../../generated/entity/teams/user';
|
||||
import { EntityReference } from '../../generated/type/entityReference';
|
||||
import { Paging } from '../../generated/type/paging';
|
||||
@ -46,6 +46,8 @@ export interface TopicDetailsProps {
|
||||
feedCount: number;
|
||||
entityFieldThreadCount: EntityFieldThreadCount[];
|
||||
paging: Paging;
|
||||
isSampleDataLoading?: boolean;
|
||||
sampleData?: TopicSampleData;
|
||||
fetchFeedHandler: (after?: string) => void;
|
||||
createThread: (data: CreateThread) => void;
|
||||
setActiveTabHandler: (value: number) => void;
|
||||
|
||||
@ -56,7 +56,7 @@ const TopicDetailsProps = {
|
||||
maximumMessageSize: 0,
|
||||
replicationFactor: 0,
|
||||
retentionSize: 0,
|
||||
schemaText: '',
|
||||
schemaText: 'schema text',
|
||||
schemaType: 'Avro',
|
||||
serviceType: '',
|
||||
users: [],
|
||||
@ -192,13 +192,25 @@ describe('Test TopicDetails component', () => {
|
||||
expect(activityFeedList).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Check if active tab is config', async () => {
|
||||
it('Check if active tab is sample data', async () => {
|
||||
const { container } = render(
|
||||
<TopicDetails {...TopicDetailsProps} activeTab={3} />,
|
||||
{
|
||||
wrapper: MemoryRouter,
|
||||
}
|
||||
);
|
||||
const sampleData = await findByTestId(container, 'sample-data');
|
||||
|
||||
expect(sampleData).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Check if active tab is config', async () => {
|
||||
const { container } = render(
|
||||
<TopicDetails {...TopicDetailsProps} activeTab={4} />,
|
||||
{
|
||||
wrapper: MemoryRouter,
|
||||
}
|
||||
);
|
||||
const config = await findByTestId(container, 'config');
|
||||
|
||||
expect(config).toBeInTheDocument();
|
||||
@ -206,7 +218,7 @@ describe('Test TopicDetails component', () => {
|
||||
|
||||
it('Check if active tab is manage', async () => {
|
||||
const { container } = render(
|
||||
<TopicDetails {...TopicDetailsProps} activeTab={4} />,
|
||||
<TopicDetails {...TopicDetailsProps} activeTab={5} />,
|
||||
{
|
||||
wrapper: MemoryRouter,
|
||||
}
|
||||
|
||||
@ -88,6 +88,10 @@ export interface Topic {
|
||||
* Retention time in milliseconds. For Kafka - `retention.ms` configuration.
|
||||
*/
|
||||
retentionTime?: number;
|
||||
/**
|
||||
* Sample data for a topic.
|
||||
*/
|
||||
sampleData?: TopicSampleData;
|
||||
/**
|
||||
* Schema used for message serialization. Optional as some topics may not have associated
|
||||
* schemas.
|
||||
@ -227,6 +231,18 @@ export interface EntityReference {
|
||||
type: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sample data for a topic.
|
||||
*
|
||||
* This schema defines the type to capture sample data for a topic.
|
||||
*/
|
||||
export interface TopicSampleData {
|
||||
/**
|
||||
* List of local sample messages for a topic.
|
||||
*/
|
||||
messages?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema used for message serialization.
|
||||
*
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { compare } from 'fast-json-patch';
|
||||
import { startCase } from 'lodash';
|
||||
import { isUndefined, startCase } from 'lodash';
|
||||
import { observer } from 'mobx-react';
|
||||
import { EntityFieldThreadCount, EntityTags, EntityThread } from 'Models';
|
||||
import React, { FunctionComponent, useEffect, useState } from 'react';
|
||||
@ -43,7 +43,7 @@ import {
|
||||
import { EntityType, TabSpecificField } from '../../enums/entity.enum';
|
||||
import { ServiceCategory } from '../../enums/service.enum';
|
||||
import { CreateThread } from '../../generated/api/feed/createThread';
|
||||
import { Topic } from '../../generated/entity/data/topic';
|
||||
import { Topic, TopicSampleData } from '../../generated/entity/data/topic';
|
||||
import { EntityReference } from '../../generated/type/entityReference';
|
||||
import { Paging } from '../../generated/type/paging';
|
||||
import { TagLabel } from '../../generated/type/tagLabel';
|
||||
@ -103,6 +103,10 @@ const TopicDetailsPage: FunctionComponent = () => {
|
||||
>([]);
|
||||
const [paging, setPaging] = useState<Paging>({} as Paging);
|
||||
|
||||
const [sampleData, setSampleData] = useState<TopicSampleData>();
|
||||
const [isSampleDataLoading, setIsSampleDataLoading] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const activeTabHandler = (tabValue: number) => {
|
||||
const currentTabIndex = tabValue - 1;
|
||||
if (topicDetailsTabs[currentTabIndex].path !== tab) {
|
||||
@ -159,6 +163,47 @@ const TopicDetailsPage: FunctionComponent = () => {
|
||||
.finally(() => setIsentityThreadLoading(false));
|
||||
};
|
||||
|
||||
const fetchTabSpecificData = (tabField = '') => {
|
||||
switch (tabField) {
|
||||
case TabSpecificField.ACTIVITY_FEED: {
|
||||
fetchActivityFeed();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case TabSpecificField.SAMPLE_DATA: {
|
||||
if (!isUndefined(sampleData)) {
|
||||
break;
|
||||
} else {
|
||||
setIsSampleDataLoading(true);
|
||||
getTopicByFqn(topicFQN, tabField)
|
||||
.then((res: AxiosResponse) => {
|
||||
if (res.data) {
|
||||
const { sampleData } = res.data;
|
||||
setSampleData(sampleData);
|
||||
} else {
|
||||
showErrorToast(
|
||||
jsonData['api-error-messages']['fetch-sample-data-error']
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
showErrorToast(
|
||||
err,
|
||||
jsonData['api-error-messages']['fetch-sample-data-error']
|
||||
);
|
||||
})
|
||||
.finally(() => setIsSampleDataLoading(false));
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const saveUpdatedTopicData = (updatedData: Topic): Promise<AxiosResponse> => {
|
||||
const jsonPatch = compare(topicDetails, updatedData);
|
||||
|
||||
@ -478,13 +523,13 @@ const TopicDetailsPage: FunctionComponent = () => {
|
||||
if (topicDetailsTabs[activeTab - 1].path !== tab) {
|
||||
setActiveTab(getCurrentTopicTab(tab));
|
||||
}
|
||||
if (TabSpecificField.ACTIVITY_FEED === tab) {
|
||||
fetchActivityFeed();
|
||||
} else {
|
||||
setEntityThread([]);
|
||||
}
|
||||
setEntityThread([]);
|
||||
}, [tab]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTabSpecificData(topicDetailsTabs[activeTab - 1].field);
|
||||
}, [activeTab]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTopicDetail(topicFQN);
|
||||
getEntityFeedCount();
|
||||
@ -514,6 +559,7 @@ const TopicDetailsPage: FunctionComponent = () => {
|
||||
fetchFeedHandler={fetchActivityFeed}
|
||||
followTopicHandler={followTopic}
|
||||
followers={followers}
|
||||
isSampleDataLoading={isSampleDataLoading}
|
||||
isentityThreadLoading={isentityThreadLoading}
|
||||
maximumMessageSize={maximumMessageSize}
|
||||
owner={owner as EntityReference}
|
||||
@ -522,6 +568,7 @@ const TopicDetailsPage: FunctionComponent = () => {
|
||||
postFeedHandler={postFeedHandler}
|
||||
replicationFactor={replicationFactor}
|
||||
retentionSize={retentionSize}
|
||||
sampleData={sampleData}
|
||||
schemaText={schemaText}
|
||||
schemaType={schemaType}
|
||||
setActiveTabHandler={activeTabHandler}
|
||||
|
||||
@ -931,3 +931,17 @@ code {
|
||||
.page-not-found-container::before {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.topic-sample-data > .CodeMirror {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.topic-sample-data-message {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
line-height: 18px;
|
||||
max-height: 54px;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
@ -83,6 +83,8 @@ export const bytesToSize = (bytes: number) => {
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
if (bytes === 0) {
|
||||
return `${bytes} ${sizes[0]}`;
|
||||
} else if (bytes < 0) {
|
||||
return `N/A`;
|
||||
} else {
|
||||
const i = parseInt(
|
||||
Math.floor(Math.log(bytes) / Math.log(1024)).toString(),
|
||||
|
||||
@ -23,6 +23,11 @@ export const topicDetailsTabs = [
|
||||
path: 'activity_feed',
|
||||
field: TabSpecificField.ACTIVITY_FEED,
|
||||
},
|
||||
{
|
||||
name: 'Sample Data',
|
||||
path: 'sample_data',
|
||||
field: TabSpecificField.SAMPLE_DATA,
|
||||
},
|
||||
{
|
||||
name: 'Config',
|
||||
path: 'config',
|
||||
@ -40,13 +45,17 @@ export const getCurrentTopicTab = (tab: string) => {
|
||||
currentTab = 2;
|
||||
|
||||
break;
|
||||
case 'config':
|
||||
case 'sample_data':
|
||||
currentTab = 3;
|
||||
|
||||
break;
|
||||
case 'manage':
|
||||
case 'config':
|
||||
currentTab = 4;
|
||||
|
||||
break;
|
||||
case 'manage':
|
||||
currentTab = 5;
|
||||
|
||||
break;
|
||||
|
||||
case 'schema':
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user