diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AddService/AddService.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AddService/AddService.test.tsx index 07843bd96b0..552c55ef6e6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AddService/AddService.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AddService/AddService.test.tsx @@ -40,10 +40,6 @@ jest.mock('../common/ServiceDocPanel/ServiceDocPanel', () => { return jest.fn().mockReturnValue(
ServiceDocPanel
); }); -jest.mock('../common/ServiceRightPanel/ServiceRightPanel', () => { - return jest.fn().mockReturnValue(
Right Panel
); -}); - jest.mock('components/common/ResizablePanels/ResizablePanels', () => jest.fn().mockImplementation(({ firstPanel, secondPanel }) => ( <> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Modals/WhatsNewModal/ChangeLogs.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Modals/WhatsNewModal/ChangeLogs.tsx index 63524e1a41b..c60d5af3d8f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Modals/WhatsNewModal/ChangeLogs.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Modals/WhatsNewModal/ChangeLogs.tsx @@ -33,7 +33,6 @@ const ChangeLogs = ({ data }: Props) => { ))} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/ServiceDocPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/ServiceDocPanel.tsx index aa2d8489837..02cc0ecaf72 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/ServiceDocPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/ServiceDocPanel.tsx @@ -132,7 +132,6 @@ const ServiceDocPanel: FC = ({ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceRightPanel/ServiceRightPanel.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceRightPanel/ServiceRightPanel.interface.ts deleted file mode 100644 index 0907103db1d..00000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceRightPanel/ServiceRightPanel.interface.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2023 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 { ServiceCategory } from 'enums/service.enum'; -import { PipelineType } from 'generated/api/services/ingestionPipelines/createIngestionPipeline'; - -export type ExcludedPipelineType = Exclude< - PipelineType, - | PipelineType.DataInsight - | PipelineType.ElasticSearchReindex - | PipelineType.TestSuite ->; - -export interface RightPanelProps { - activeStep: number; - isIngestion: boolean; - serviceName: string; - isUpdating: boolean; - selectedService: string; - selectedServiceCategory: ServiceCategory; - showDeployedTitle?: boolean; - pipelineType?: ExcludedPipelineType; - ingestionName?: string; - activeField?: string; -} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceRightPanel/ServiceRightPanel.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceRightPanel/ServiceRightPanel.test.tsx deleted file mode 100644 index df19e76d070..00000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceRightPanel/ServiceRightPanel.test.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2023 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 { act, render, screen } from '@testing-library/react'; -import { ServiceCategory } from 'enums/service.enum'; -import { PipelineType } from 'generated/api/services/ingestionPipelines/createIngestionPipeline'; -import React from 'react'; -import RightPanel from './ServiceRightPanel'; -import { ExcludedPipelineType } from './ServiceRightPanel.interface'; - -jest.mock('components/common/rich-text-editor/RichTextEditorPreviewer', () => - jest - .fn() - .mockImplementation(({ markdown }) => ( -
{markdown}
- )) -); - -jest.mock('rest/miscAPI', () => ({ - fetchMarkdownFile: jest - .fn() - .mockImplementation(() => Promise.resolve('markdown text')), -})); - -const mockProps = { - isIngestion: false, - pipelineType: PipelineType.Metadata as ExcludedPipelineType, - activeStep: 1, - isAirflowRunning: true, - showDeployedTitle: true, - isUpdating: false, - ingestionName: 'service_ingestion', - serviceName: 'service', - activeField: 'root_username', - selectedServiceCategory: ServiceCategory.DATABASE_SERVICES, - selectedService: 'Mysql', -}; - -describe('Right Panel Component', () => { - it('Should render the active field doc', async () => { - await act(async () => { - render(); - }); - - const activeFieldName = screen.getByTestId('active-field-name'); - - expect(activeFieldName).toBeInTheDocument(); - - expect(activeFieldName).toHaveTextContent('Username'); - - const activeFieldDocumentElement = screen.getByTestId('requirement-text'); - - expect(activeFieldDocumentElement).toBeInTheDocument(); - - expect(activeFieldDocumentElement).toHaveTextContent('markdown text'); - }); - - it('Should render the current step guide if active field is empty', async () => { - await act(async () => { - render(); - }); - - expect(screen.queryByTestId('active-field-name')).not.toBeInTheDocument(); - - const activeFieldDocumentElement = screen.queryByTestId('requirement-text'); - - expect(activeFieldDocumentElement).not.toBeInTheDocument(); - - expect(screen.getByText('label.add-a-new-service')).toBeInTheDocument(); - - expect( - screen.getByText('message.add-new-service-description') - ).toBeInTheDocument(); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceRightPanel/ServiceRightPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceRightPanel/ServiceRightPanel.tsx deleted file mode 100644 index 027c6eb646a..00000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceRightPanel/ServiceRightPanel.tsx +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Copyright 2023 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 { Affix, Card } from 'antd'; -import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichTextEditorPreviewer'; -import Loader from 'components/Loader/Loader'; -import { oneofOrEndsWithNumberRegex } from 'constants/regex.constants'; -import { - addServiceGuide, - addServiceGuideWOAirflow, - EMAIL_CONFIG_SERVICE_CATEGORY, -} from 'constants/service-guide.constant'; -import { INGESTION_GUIDE_MAP } from 'constants/Services.constant'; -import { useAirflowStatus } from 'hooks/useAirflowStatus'; -import { first, last, startCase } from 'lodash'; -import React, { FC, useEffect, useMemo, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { fetchMarkdownFile } from 'rest/miscAPI'; -import { SupportedLocales } from 'utils/i18next/i18nextUtil'; -import { getEmailConfigStepGuide } from 'utils/ServiceRightPanelUtils'; -import { getFormattedGuideText, getServiceType } from 'utils/ServiceUtils'; -import { RightPanelProps } from './ServiceRightPanel.interface'; - -const RightPanel: FC = ({ - isIngestion, - pipelineType, - activeStep, - isUpdating, - ingestionName, - serviceName, - activeField, - selectedServiceCategory, - selectedService, - showDeployedTitle = false, -}) => { - const panelContainerRef = useRef(null); - - const { isAirflowAvailable } = useAirflowStatus(); - const { t, i18n } = useTranslation(); - - const [activeFieldDocument, setActiveFieldDocument] = useState(''); - const [isLoading, setIsLoading] = useState(false); - - const isEmailConfigPage = useMemo( - () => selectedService === EMAIL_CONFIG_SERVICE_CATEGORY, - [selectedService] - ); - - const activeStepGuide = useMemo(() => { - let guideTemp; - - if (isIngestion && pipelineType) { - guideTemp = INGESTION_GUIDE_MAP[pipelineType]?.find( - (item) => item.step === activeStep - ); - } else { - guideTemp = - !isAirflowAvailable && activeStep === 4 - ? addServiceGuideWOAirflow - : addServiceGuide.find((item) => item.step === activeStep); - } - - return guideTemp; - }, [isIngestion, pipelineType, isAirflowAvailable, activeStep]); - - const activeFieldName = useMemo(() => { - /** - * active field is like root_fieldName - * so we need to split and get the fieldName - */ - const fieldNameArr = activeField?.split('/'); - - const fieldName = last(fieldNameArr); - - // check if activeField is select or list field - if (oneofOrEndsWithNumberRegex.test(fieldName ?? '')) { - return first(fieldName?.split('_')); - } else { - return fieldName; - } - }, [activeField]); - - const showActiveFieldElement = Boolean( - activeFieldName && activeFieldDocument - ); - - const getActiveStepTitle = (title: string) => { - const deployMessage = showDeployedTitle ? ` & ${t('label.deployed')}` : ''; - const updateTitle = title.replace( - t('label.added'), - `${t('label.updated')}${deployMessage}` - ); - const newTitle = showDeployedTitle - ? title.replace(t('label.added'), `${t('label.added')}${deployMessage}`) - : title; - - return isUpdating ? updateTitle : newTitle; - }; - - const getActiveStepDescription = (description: string) => { - const replaceText = isIngestion - ? `<${t('label.ingestion-pipeline-name')}>` - : `<${t('label.service-name')}>`; - - const replacement = isIngestion ? ingestionName || '' : serviceName; - - return getFormattedGuideText(description, replaceText, replacement); - }; - - const fetchFieldDocument = async () => { - const serviceType = isEmailConfigPage - ? selectedServiceCategory - : getServiceType(selectedServiceCategory); - setIsLoading(true); - try { - let response = ''; - const isEnglishLanguage = i18n.language === SupportedLocales.English; - const filePath = `${i18n.language}/${serviceType}/${selectedService}/fields/${activeFieldName}.md`; - const fallbackFilePath = `${SupportedLocales.English}/${serviceType}/${selectedService}/fields/${activeFieldName}.md`; - - const [translation, fallbackTranslation] = await Promise.allSettled([ - fetchMarkdownFile(filePath), - isEnglishLanguage - ? Promise.reject('') - : fetchMarkdownFile(fallbackFilePath), - ]); - - if (translation.status === 'fulfilled') { - response = translation.value; - } - - if (isEnglishLanguage && fallbackTranslation.status === 'fulfilled') { - response = fallbackTranslation.value; - } - - setActiveFieldDocument(response); - } catch (error) { - setActiveFieldDocument(''); - } finally { - setIsLoading(false); - } - }; - - const handleAffixTarget = () => document.getElementById('page-container-v1'); - - useEffect(() => { - const shouldFetchFieldDoc = Boolean( - selectedService && selectedServiceCategory && activeFieldName - ); - // only fetch file when required fields are present - if (shouldFetchFieldDoc) { - fetchFieldDocument(); - } - }, [ - selectedService, - selectedServiceCategory, - activeFieldName, - isEmailConfigPage, - ]); - - const activeStepGuideElement = activeStepGuide ? ( - <> -
- {getActiveStepTitle(activeStepGuide.title)} -
-
- {getActiveStepDescription(activeStepGuide.description)} -
- - ) : null; - - const activeFieldDocumentElement = activeFieldName ? ( - <> -
- {startCase(activeFieldName)} -
- - - ) : null; - - const renderElement = useMemo(() => { - if (showActiveFieldElement) { - return activeFieldDocumentElement; - } else { - return isEmailConfigPage - ? getEmailConfigStepGuide() - : activeStepGuideElement; - } - }, [ - showActiveFieldElement, - activeFieldDocumentElement, - activeStepGuideElement, - selectedService, - isEmailConfigPage, - ]); - - return ( -
- - {isLoading ? : renderElement} - -
- ); -}; - -export default RightPanel; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditorPreviewer.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditorPreviewer.test.tsx index 0efe2e4b380..b2d02c80ef1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditorPreviewer.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditorPreviewer.test.tsx @@ -11,22 +11,28 @@ * limitations under the License. */ -import { findByTestId, fireEvent, render } from '@testing-library/react'; +import { + act, + findByTestId, + fireEvent, + render, + screen, +} from '@testing-library/react'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { act } from 'react-test-renderer'; + +import userEvent from '@testing-library/user-event'; +import { PreviewerProp } from './RichTextEditor.interface'; import RichTextEditorPreviewer from './RichTextEditorPreviewer'; const mockDescription = // eslint-disable-next-line max-len '**Headings**\n\n# H1\n## H2\n### H3\n\n***\n**Bold**\n\n**bold text**\n\n\n***\n**Italic**\n\n*italic*\n\n***\n**BlockQuote**\n\n> blockquote\n\n***\n**Ordered List**\n\n1. First item\n2. Second item\n3. Third item\n\n\n***\n**Unordered List**\n\n- First item\n- Second item\n- Third item\n\n\n***\n**Code**\n\n`code`\n\n\n***\n**Horizontal Rule**\n\n---\n\n\n***\n**Link**\n[title](https://www.example.com)\n\n\n***\n**Image**\n\n![alt text](https://github.com/open-metadata/OpenMetadata/blob/main/docs/.gitbook/assets/openmetadata-banner.png?raw=true)\n\n\n***\n**Table**\n\n| Syntax | Description |\n| ----------- | ----------- |\n| Header | Title |\n| Paragraph | Text |\n***\n\n**Fenced Code Block**\n\n```\n{\n "firstName": "John",\n "lastName": "Smith",\n "age": 25\n}\n```\n\n\n***\n**Strikethrough**\n~~The world is flat.~~\n'; -const mockProp = { +const mockProp: PreviewerProp = { markdown: mockDescription, className: '', - blurClasses: 'see-more-blur', - maxHtClass: 'tw-h-24', - maxLen: 300, + maxLength: 300, enableSeeMoreVariant: true, }; @@ -344,4 +350,89 @@ describe('Test RichTextEditor Previewer Component', () => { expect(markdownParser).toBeInTheDocument(); }); + + it('Should render read more button if enableSeeMoreVariant is true and max length is less than content length', () => { + render(, { + wrapper: MemoryRouter, + }); + + expect(screen.getByTestId('read-more-button')).toBeInTheDocument(); + }); + + it('Read more toggling should work', async () => { + render(, { + wrapper: MemoryRouter, + }); + + const readMoreButton = screen.getByTestId('read-more-button'); + + await act(async () => { + userEvent.click(readMoreButton); + }); + + const readLessButton = screen.getByTestId('read-less-button'); + + expect(readLessButton).toBeInTheDocument(); + + await act(async () => { + userEvent.click(readLessButton); + }); + + expect(screen.getByTestId('read-more-button')).toBeInTheDocument(); + }); + + it('Should render the whole content if enableSeeMoreVariant is false', () => { + const markdown = 'This is a simple paragraph text'; + + render( + , + { + wrapper: MemoryRouter, + } + ); + + expect(screen.getByText(markdown)).toBeInTheDocument(); + expect(screen.queryByTestId('read-more-button')).toBeNull(); + }); + + it('Should render the clipped content if enableSeeMoreVariant is true', () => { + const markdown = 'This is a simple paragraph text'; + + render( + , + { + wrapper: MemoryRouter, + } + ); + + expect(screen.getByText('This is a simple...')).toBeInTheDocument(); + expect(screen.queryByTestId('read-more-button')).toBeInTheDocument(); + }); + + it('Should not clipped content if enableSeeMoreVariant is true and markdown length is less than max length', () => { + const markdown = 'This is a simple paragraph text'; + + render( + , + { + wrapper: MemoryRouter, + } + ); + + expect(screen.getByText(markdown)).toBeInTheDocument(); + expect(screen.queryByTestId('read-more-button')).toBeNull(); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditorPreviewer.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditorPreviewer.tsx index cd8bbdef49c..57fb7d267ea 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditorPreviewer.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditorPreviewer.tsx @@ -15,7 +15,7 @@ import { Viewer } from '@toast-ui/react-editor'; import { Button } from 'antd'; import classNames from 'classnames'; import { uniqueId } from 'lodash'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { getTrimmedContent } from 'utils/CommonUtils'; import { DESCRIPTION_MAX_PREVIEW_CHARACTERS } from '../../../constants/constants'; @@ -32,13 +32,30 @@ const RichTextEditorPreviewer = ({ }: PreviewerProp) => { const { t } = useTranslation(); const [content, setContent] = useState(''); - const [hideReadMoreText, setHideReadMoreText] = useState( - markdown.length <= maxLength + + // initially read more will be false + const [readMore, setReadMore] = useState(false); + + // read more toggle handler + const handleReadMoreToggle = () => setReadMore((pre) => !pre); + + // whether has read more content or not + const hasReadMore = useMemo( + () => enableSeeMoreVariant && markdown.length > maxLength, + [enableSeeMoreVariant, markdown, maxLength] ); - const displayMoreHandler = () => { - setHideReadMoreText((pre) => !pre); - }; + /** + * if hasReadMore is true then value will be based on read more state + * else value will be content + */ + const viewerValue = useMemo(() => { + if (hasReadMore) { + return readMore ? content : `${getTrimmedContent(content, maxLength)}...`; + } + + return content; + }, [hasReadMore, readMore, maxLength, content]); useEffect(() => { setContent(markdown); @@ -83,22 +100,18 @@ const RichTextEditorPreviewer = ({ - {enableSeeMoreVariant && markdown.length > maxLength && ( + {hasReadMore && (