feat(ui)#9809: <title> in HTML heading should reflect the page you're on (#10342)

* feat(ui)#9809: <title> in HTML heading should reflect the page you're on

* chore: add dynamic title for pageLayoutV1

* chore: create separate component for seo

* address comments

* test: add new test and fix failing test
This commit is contained in:
Sachin Chaurasiya 2023-02-28 17:02:32 +05:30 committed by GitHub
parent 5865466094
commit 82df166d4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 395 additions and 76 deletions

View File

@ -74,6 +74,7 @@
"react-dnd-html5-backend": "14.0.2", "react-dnd-html5-backend": "14.0.2",
"react-dom": "^16.14.0", "react-dom": "^16.14.0",
"react-error-boundary": "^3.1.4", "react-error-boundary": "^3.1.4",
"react-helmet-async": "^1.3.0",
"react-i18next": "^11.18.6", "react-i18next": "^11.18.6",
"react-lazylog": "^4.5.3", "react-lazylog": "^4.5.3",
"react-oidc": "^1.0.3", "react-oidc": "^1.0.3",
@ -180,8 +181,8 @@
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"eslint": "7.32.0", "eslint": "7.32.0",
"eslint-config-prettier": "8.3.0", "eslint-config-prettier": "8.3.0",
"eslint-plugin-i18next": "^6.0.0-2",
"eslint-plugin-cypress": "^2.12.1", "eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-i18next": "^6.0.0-2",
"eslint-plugin-jest": "24.4.0", "eslint-plugin-jest": "24.4.0",
"eslint-plugin-jest-formatting": "3.0.0", "eslint-plugin-jest-formatting": "3.0.0",
"eslint-plugin-jsonc": "2.5.0", "eslint-plugin-jsonc": "2.5.0",

View File

@ -21,6 +21,7 @@ import WebSocketProvider from 'components/web-scoket/web-scoket.provider';
import WebAnalyticsProvider from 'components/WebAnalytics/WebAnalyticsProvider'; import WebAnalyticsProvider from 'components/WebAnalytics/WebAnalyticsProvider';
import { TOAST_OPTIONS } from 'constants/Toasts.constants'; import { TOAST_OPTIONS } from 'constants/Toasts.constants';
import React, { FunctionComponent } from 'react'; import React, { FunctionComponent } from 'react';
import { HelmetProvider } from 'react-helmet-async';
import { I18nextProvider } from 'react-i18next'; import { I18nextProvider } from 'react-i18next';
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
import { ToastContainer } from 'react-toastify'; import { ToastContainer } from 'react-toastify';
@ -35,16 +36,18 @@ const App: FunctionComponent = () => {
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<ErrorBoundry> <ErrorBoundry>
<AuthProvider childComponentType={AppRouter}> <AuthProvider childComponentType={AppRouter}>
<WebAnalyticsProvider> <HelmetProvider>
<PermissionProvider> <WebAnalyticsProvider>
<WebSocketProvider> <PermissionProvider>
<GlobalSearchProvider> <WebSocketProvider>
<Appbar /> <GlobalSearchProvider>
<AppRouter /> <Appbar />
</GlobalSearchProvider> <AppRouter />
</WebSocketProvider> </GlobalSearchProvider>
</PermissionProvider> </WebSocketProvider>
</WebAnalyticsProvider> </PermissionProvider>
</WebAnalyticsProvider>
</HelmetProvider>
</AuthProvider> </AuthProvider>
</ErrorBoundry> </ErrorBoundry>
</I18nextProvider> </I18nextProvider>

View File

@ -255,6 +255,9 @@ const AddDataQualityTestV1: React.FC<AddDataQualityTestProps> = ({
classes="tw-max-w-full-hd tw-h-full tw-pt-4" classes="tw-max-w-full-hd tw-h-full tw-pt-4"
header={<TitleBreadcrumb titleLinks={breadcrumb} />} header={<TitleBreadcrumb titleLinks={breadcrumb} />}
layout={PageLayoutType['2ColRTL']} layout={PageLayoutType['2ColRTL']}
pageTitle={t('label.add-entity', {
entity: t('label.data-quality-test'),
})}
rightPanel={ rightPanel={
<RightPanel <RightPanel
data={ data={

View File

@ -155,6 +155,7 @@ const AddGlossary = ({
classes="tw-max-w-full-hd tw-h-full tw-pt-4" classes="tw-max-w-full-hd tw-h-full tw-pt-4"
header={<TitleBreadcrumb titleLinks={slashedBreadcrumb} />} header={<TitleBreadcrumb titleLinks={slashedBreadcrumb} />}
layout={PageLayoutType['2ColRTL']} layout={PageLayoutType['2ColRTL']}
pageTitle={t('label.add-entity', { entity: t('label.glossary') })}
rightPanel={fetchRightPanel()}> rightPanel={fetchRightPanel()}>
<div className="tw-form-container"> <div className="tw-form-container">
<Typography.Title data-testid="form-heading" level={5}> <Typography.Title data-testid="form-heading" level={5}>

View File

@ -16,6 +16,10 @@ import { LoadingState } from 'Models';
import React, { forwardRef } from 'react'; import React, { forwardRef } from 'react';
import AddGlossary from './AddGlossary.component'; import AddGlossary from './AddGlossary.component';
jest.mock('../containers/PageLayout', () =>
jest.fn().mockImplementation(({ children }) => <div>{children}</div>)
);
jest.mock('../common/rich-text-editor/RichTextEditorPreviewer', () => { jest.mock('../common/rich-text-editor/RichTextEditorPreviewer', () => {
return jest.fn().mockReturnValue(<p>RichTextEditorPreviewer</p>); return jest.fn().mockReturnValue(<p>RichTextEditorPreviewer</p>);
}); });

View File

@ -296,6 +296,7 @@ const AddGlossaryTerm = ({
classes="tw-max-w-full-hd tw-h-full tw-pt-4" classes="tw-max-w-full-hd tw-h-full tw-pt-4"
header={<TitleBreadcrumb titleLinks={slashedBreadcrumb} />} header={<TitleBreadcrumb titleLinks={slashedBreadcrumb} />}
layout={PageLayoutType['2ColRTL']} layout={PageLayoutType['2ColRTL']}
pageTitle={t('label.add-entity', { entity: t('label.glossary-term') })}
rightPanel={fetchRightPanel()}> rightPanel={fetchRightPanel()}>
<div className="tw-form-container"> <div className="tw-form-container">
<h6 className="tw-heading tw-text-base"> <h6 className="tw-heading tw-text-base">

View File

@ -53,6 +53,10 @@ jest.mock('../common/rich-text-editor/RichTextEditor', () => {
); );
}); });
jest.mock('../containers/PageLayout', () =>
jest.fn().mockImplementation(({ children }) => <div>{children}</div>)
);
const mockOnCancel = jest.fn(); const mockOnCancel = jest.fn();
const mockOnSave = jest.fn(); const mockOnSave = jest.fn();

View File

@ -320,6 +320,7 @@ const AddService = ({
classes="tw-max-w-full-hd tw-h-full tw-pt-4" classes="tw-max-w-full-hd tw-h-full tw-pt-4"
header={<TitleBreadcrumb titleLinks={slashedBreadcrumb} />} header={<TitleBreadcrumb titleLinks={slashedBreadcrumb} />}
layout={PageLayoutType['2ColRTL']} layout={PageLayoutType['2ColRTL']}
pageTitle={t('label.add-entity', { entity: t('label.service') })}
rightPanel={fetchRightPanel()}> rightPanel={fetchRightPanel()}>
<div className="tw-form-container"> <div className="tw-form-container">
{addIngestion ? ( {addIngestion ? (

View File

@ -276,6 +276,7 @@ const BotDetails: FC<BotsDetailProps> = ({
/> />
} }
leftPanel={fetchLeftPanel()} leftPanel={fetchLeftPanel()}
pageTitle={t('label.bot-detail')}
rightPanel={ rightPanel={
<Card className="page-layout-v1-left-panel mt-2"> <Card className="page-layout-v1-left-panel mt-2">
<div data-testid="right-panel"> <div data-testid="right-panel">

View File

@ -122,6 +122,19 @@ jest.mock('./AuthMechanismForm', () =>
) )
); );
jest.mock('../containers/PageLayout', () =>
jest
.fn()
.mockImplementation(({ children, leftPanel, rightPanel, header }) => (
<div>
{header}
<div>{leftPanel}</div>
{children}
<div>{rightPanel}</div>
</div>
))
);
describe('Test BotsDetail Component', () => { describe('Test BotsDetail Component', () => {
it('Should render all child elements', async () => { it('Should render all child elements', async () => {
const { container } = render(<BotDetails {...mockProp} />, { const { container } = render(<BotDetails {...mockProp} />, {

View File

@ -700,7 +700,8 @@ const CreateUser = ({
<PageLayout <PageLayout
classes="tw-max-w-full-hd tw-h-full tw-pt-4" classes="tw-max-w-full-hd tw-h-full tw-pt-4"
header={<TitleBreadcrumb titleLinks={slashedBreadcrumbList} />} header={<TitleBreadcrumb titleLinks={slashedBreadcrumbList} />}
layout={PageLayoutType['2ColRTL']}> layout={PageLayoutType['2ColRTL']}
pageTitle={t('label.create-entity', { entity: t('label.user') })}>
<div className="tw-form-container"> <div className="tw-form-container">
<h6 className="tw-heading tw-text-base"> <h6 className="tw-heading tw-text-base">
{t('label.create-entity', { {t('label.create-entity', {

View File

@ -174,6 +174,9 @@ const AddCustomProperty = () => {
<PageLayout <PageLayout
classes="tw-max-w-full-hd tw-h-full tw-pt-4" classes="tw-max-w-full-hd tw-h-full tw-pt-4"
layout={PageLayoutType['2ColRTL']} layout={PageLayoutType['2ColRTL']}
pageTitle={t('label.add-entity', {
entity: t('label.custom-property'),
})}
rightPanel={<RightPanel />}> rightPanel={<RightPanel />}>
<div <div
className="tw-bg-white tw-p-4 tw-border tw-border-main tw-rounded tw-form-container" className="tw-bg-white tw-p-4 tw-border tw-border-main tw-rounded tw-form-container"

View File

@ -0,0 +1,31 @@
/*
* 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 React, { FC } from 'react';
import { Helmet } from 'react-helmet-async';
import { useTranslation } from 'react-i18next';
interface DocumentTitleProps {
title: string;
}
const DocumentTitle: FC<DocumentTitleProps> = ({ title }) => {
const { t } = useTranslation();
return (
<Helmet>
<title>{`${title} | ${t('label.open-metadata')}`}</title>
</Helmet>
);
};
export default DocumentTitle;

View File

@ -31,6 +31,7 @@ import {
toUpper, toUpper,
} from 'lodash'; } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { ENTITY_PATH } from '../../constants/constants'; import { ENTITY_PATH } from '../../constants/constants';
import { tabsInfo } from '../../constants/explore.constants'; import { tabsInfo } from '../../constants/explore.constants';
@ -76,6 +77,7 @@ const Explore: React.FC<ExploreProps> = ({
onChangePage = noop, onChangePage = noop,
loading, loading,
}) => { }) => {
const { t } = useTranslation();
const { tab } = useParams<{ tab: string }>(); const { tab } = useParams<{ tab: string }>();
const [showAdvanceSearchModal, setShowAdvanceSearchModal] = useState(false); const [showAdvanceSearchModal, setShowAdvanceSearchModal] = useState(false);
@ -268,7 +270,8 @@ const Explore: React.FC<ExploreProps> = ({
/> />
</ExploreSkeleton> </ExploreSkeleton>
</Card> </Card>
}> }
pageTitle={t('label.explore')}>
<Tabs <Tabs
defaultActiveKey={defaultActiveTab} defaultActiveKey={defaultActiveTab}
items={tabItems} items={tabItems}

View File

@ -49,6 +49,10 @@ jest.mock('components/searched-data/SearchedData', () => {
const mockFunction = jest.fn(); const mockFunction = jest.fn();
jest.mock('../containers/PageLayoutV1', () =>
jest.fn().mockImplementation(({ children }) => <div>{children}</div>)
);
describe('Test Explore component', () => { describe('Test Explore component', () => {
it('Component should render', async () => { it('Component should render', async () => {
const { container } = render( const { container } = render(

View File

@ -12,6 +12,7 @@
*/ */
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import PageLayoutV1 from '../containers/PageLayoutV1'; import PageLayoutV1 from '../containers/PageLayoutV1';
import GlobalSettingRouter from '../router/GlobalSettingRouter'; import GlobalSettingRouter from '../router/GlobalSettingRouter';
@ -19,10 +20,13 @@ import './GlobalSetting.less';
import GlobalSettingLeftPanel from './GlobalSettingLeftPanel'; import GlobalSettingLeftPanel from './GlobalSettingLeftPanel';
const GlobalSetting = () => { const GlobalSetting = () => {
const { t } = useTranslation();
return ( return (
<PageLayoutV1 <PageLayoutV1
className="tw-h-full tw-px-6" className="tw-h-full tw-px-6"
leftPanel={<GlobalSettingLeftPanel />}> leftPanel={<GlobalSettingLeftPanel />}
pageTitle={t('label.setting-plural')}>
<GlobalSettingRouter /> <GlobalSettingRouter />
</PageLayoutV1> </PageLayoutV1>
); );

View File

@ -231,7 +231,10 @@ const MyData: React.FC<MyDataProps> = ({
); );
return ( return (
<PageLayoutV1 leftPanel={getLeftPanel()} rightPanel={getRightPanel()}> <PageLayoutV1
leftPanel={getLeftPanel()}
pageTitle={t('label.my-data')}
rightPanel={getRightPanel()}>
{error ? ( {error ? (
<ErrorPlaceHolderES errorMessage={error} type="error" /> <ErrorPlaceHolderES errorMessage={error} type="error" />
) : ( ) : (

View File

@ -98,6 +98,10 @@ jest.mock('components/PermissionProvider/PermissionProvider', () => {
}; };
}); });
jest.mock('../containers/PageLayoutV1', () =>
jest.fn().mockImplementation(({ children }) => <div>{children}</div>)
);
describe('Test ProfilerDashboardPage component', () => { describe('Test ProfilerDashboardPage component', () => {
beforeEach(() => cleanup()); beforeEach(() => cleanup());
@ -107,7 +111,7 @@ describe('Test ProfilerDashboardPage component', () => {
wrapper: MemoryRouter, wrapper: MemoryRouter,
}); });
}); });
const pageContainer = await screen.findByTestId('page-layout-v1');
const profilerSwitch = await screen.findByTestId('profiler-switch'); const profilerSwitch = await screen.findByTestId('profiler-switch');
const EntityPageInfo = await screen.findByText('EntityPageInfo component'); const EntityPageInfo = await screen.findByText('EntityPageInfo component');
const ProfilerTab = await screen.findByText('ProfilerTab component'); const ProfilerTab = await screen.findByText('ProfilerTab component');
@ -116,7 +120,6 @@ describe('Test ProfilerDashboardPage component', () => {
); );
const DataQualityTab = screen.queryByText('DataQualityTab component'); const DataQualityTab = screen.queryByText('DataQualityTab component');
expect(pageContainer).toBeInTheDocument();
expect(profilerSwitch).toBeInTheDocument(); expect(profilerSwitch).toBeInTheDocument();
expect(EntityPageInfo).toBeInTheDocument(); expect(EntityPageInfo).toBeInTheDocument();
expect(ProfilerTab).toBeInTheDocument(); expect(ProfilerTab).toBeInTheDocument();
@ -134,7 +137,7 @@ describe('Test ProfilerDashboardPage component', () => {
wrapper: MemoryRouter, wrapper: MemoryRouter,
}); });
}); });
const pageContainer = await screen.findByTestId('page-layout-v1');
const profilerSwitch = await screen.findByTestId('profiler-switch'); const profilerSwitch = await screen.findByTestId('profiler-switch');
const EntityPageInfo = await screen.findByText('EntityPageInfo component'); const EntityPageInfo = await screen.findByText('EntityPageInfo component');
const ProfilerTab = screen.queryByText('ProfilerTab component'); const ProfilerTab = screen.queryByText('ProfilerTab component');
@ -144,7 +147,6 @@ describe('Test ProfilerDashboardPage component', () => {
); );
const statusDropdown = await screen.findByText('label.status'); const statusDropdown = await screen.findByText('label.status');
expect(pageContainer).toBeInTheDocument();
expect(profilerSwitch).toBeInTheDocument(); expect(profilerSwitch).toBeInTheDocument();
expect(EntityPageInfo).toBeInTheDocument(); expect(EntityPageInfo).toBeInTheDocument();
expect(DataQualityTab).toBeInTheDocument(); expect(DataQualityTab).toBeInTheDocument();
@ -163,7 +165,7 @@ describe('Test ProfilerDashboardPage component', () => {
wrapper: MemoryRouter, wrapper: MemoryRouter,
}); });
}); });
const pageContainer = await screen.findByTestId('page-layout-v1');
const profilerSwitch = await screen.findByTestId('profiler-switch'); const profilerSwitch = await screen.findByTestId('profiler-switch');
const EntityPageInfo = await screen.findByText('EntityPageInfo component'); const EntityPageInfo = await screen.findByText('EntityPageInfo component');
const ProfilerTab = await screen.findByText('ProfilerTab component'); const ProfilerTab = await screen.findByText('ProfilerTab component');
@ -172,7 +174,6 @@ describe('Test ProfilerDashboardPage component', () => {
); );
const DataQualityTab = screen.queryByText('DataQualityTab component'); const DataQualityTab = screen.queryByText('DataQualityTab component');
expect(pageContainer).toBeInTheDocument();
expect(profilerSwitch).toBeInTheDocument(); expect(profilerSwitch).toBeInTheDocument();
expect(EntityPageInfo).toBeInTheDocument(); expect(EntityPageInfo).toBeInTheDocument();
expect(ProfilerTab).toBeInTheDocument(); expect(ProfilerTab).toBeInTheDocument();
@ -231,10 +232,8 @@ describe('Test ProfilerDashboardPage component', () => {
}); });
}); });
const pageContainer = await screen.findByTestId('page-layout-v1');
const addTest = await screen.findByTestId('add-test'); const addTest = await screen.findByTestId('add-test');
expect(pageContainer).toBeInTheDocument();
expect(addTest).toBeInTheDocument(); expect(addTest).toBeInTheDocument();
await act(async () => { await act(async () => {

View File

@ -456,7 +456,7 @@ const ProfilerDashboard: React.FC<ProfilerDashboardProps> = ({
}, [table]); }, [table]);
return ( return (
<PageLayoutV1> <PageLayoutV1 pageTitle={t('label.profiler')}>
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col span={24}> <Col span={24}>
<EntityPageInfo <EntityPageInfo

View File

@ -124,6 +124,16 @@ jest.mock('rest/userAPI', () => ({
checkValidImage: jest.fn().mockImplementation(() => Promise.resolve(true)), checkValidImage: jest.fn().mockImplementation(() => Promise.resolve(true)),
})); }));
jest.mock('../containers/PageLayoutV1', () =>
jest.fn().mockImplementation(({ children, leftPanel, rightPanel }) => (
<div>
{leftPanel}
{children}
{rightPanel}
</div>
))
);
describe('Test User Component', () => { describe('Test User Component', () => {
it('Should render user component', async () => { it('Should render user component', async () => {
const { container } = render( const { container } = render(

View File

@ -951,7 +951,10 @@ const Users = ({
); );
return ( return (
<PageLayoutV1 className="tw-h-full" leftPanel={fetchLeftPanel()}> <PageLayoutV1
className="tw-h-full"
leftPanel={fetchLeftPanel()}
pageTitle={t('label.user')}>
<div className="m-b-md"> <div className="m-b-md">
<TabsPane <TabsPane
activeTab={activeTab} activeTab={activeTab}

View File

@ -0,0 +1,65 @@
/*
* 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 { render } from '@testing-library/react';
import React from 'react';
import PageLayout from './PageLayout';
jest.mock('components/DocumentTitle/DocumentTitle', () =>
jest.fn().mockImplementation(() => <div>DocumentTitle</div>)
);
describe('PageLayout', () => {
it('Should render with children', () => {
const { getByText } = render(
<PageLayout pageTitle="Test Page Title">
<div>Test Child Element</div>
</PageLayout>
);
expect(getByText('Test Child Element')).toBeInTheDocument();
});
it('Should render with left panel', () => {
const { getByText } = render(
<PageLayout
leftPanel={<div>Test Left Panel</div>}
pageTitle="Test Page Title">
<div>Test Child Element</div>
</PageLayout>
);
expect(getByText('Test Left Panel')).toBeInTheDocument();
});
it('Should render with right panel', () => {
const { getByText } = render(
<PageLayout
pageTitle="Test Page Title"
rightPanel={<div>Test Right Panel</div>}>
<div>Test Child Element</div>
</PageLayout>
);
expect(getByText('Test Right Panel')).toBeInTheDocument();
});
it('Should render with header', () => {
const { getByText } = render(
<PageLayout header={<div>Test Header</div>} pageTitle="Test Page Title">
<div>Test Child Element</div>
</PageLayout>
);
expect(getByText('Test Header')).toBeInTheDocument();
});
});

View File

@ -12,6 +12,7 @@
*/ */
import classNames from 'classnames'; import classNames from 'classnames';
import DocumentTitle from 'components/DocumentTitle/DocumentTitle';
import React, { FC, Fragment, ReactNode } from 'react'; import React, { FC, Fragment, ReactNode } from 'react';
import { PageLayoutType } from '../../enums/layout.enum'; import { PageLayoutType } from '../../enums/layout.enum';
@ -22,6 +23,7 @@ interface PageLayoutProp {
children: ReactNode; children: ReactNode;
layout?: PageLayoutType; layout?: PageLayoutType;
classes?: string; classes?: string;
pageTitle: string;
} }
export const leftPanelAntCardStyle = { export const leftPanelAntCardStyle = {
@ -40,6 +42,7 @@ const PageLayout: FC<PageLayoutProp> = ({
children, children,
rightPanel, rightPanel,
layout = PageLayoutType['3Col'], layout = PageLayoutType['3Col'],
pageTitle,
classes = '', classes = '',
}: PageLayoutProp) => { }: PageLayoutProp) => {
const getLeftPanel = () => { const getLeftPanel = () => {
@ -163,7 +166,12 @@ const PageLayout: FC<PageLayoutProp> = ({
} }
}; };
return getLayoutByType(layout); return (
<Fragment>
<DocumentTitle title={pageTitle} />
{getLayoutByType(layout)}
</Fragment>
);
}; };
export default PageLayout; export default PageLayout;

View File

@ -0,0 +1,52 @@
/*
* 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 { render } from '@testing-library/react';
import React from 'react';
import PageLayoutV1 from './PageLayoutV1';
jest.mock('components/DocumentTitle/DocumentTitle', () =>
jest.fn().mockImplementation(() => <div>DocumentTitle</div>)
);
describe('PageLayoutV1', () => {
it('Should render with the left panel, center content, and right panel', () => {
const leftPanelText = 'Left panel';
const centerText = 'Center content';
const rightPanelText = 'Right panel';
const { getByText } = render(
<PageLayoutV1
center
leftPanel={<div>{leftPanelText}</div>}
pageTitle="Test Page"
rightPanel={<div>{rightPanelText}</div>}>
{centerText}
</PageLayoutV1>
);
expect(getByText(leftPanelText)).toBeInTheDocument();
expect(getByText(centerText)).toBeInTheDocument();
expect(getByText(rightPanelText)).toBeInTheDocument();
});
it('Should render with only the center content', () => {
const centerText = 'Center content';
const { getByText, queryByTestId } = render(
<PageLayoutV1 pageTitle="Test Page">{centerText}</PageLayoutV1>
);
expect(queryByTestId('page-layout-v1')).toBeInTheDocument();
expect(getByText(centerText)).toBeInTheDocument();
expect(queryByTestId('left-panelV1')).not.toBeInTheDocument();
expect(queryByTestId('right-panelV1')).not.toBeInTheDocument();
});
});

View File

@ -13,7 +13,8 @@
import { Col, Row } from 'antd'; import { Col, Row } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { FC, HTMLAttributes, ReactNode } from 'react'; import DocumentTitle from 'components/DocumentTitle/DocumentTitle';
import React, { FC, Fragment, HTMLAttributes, ReactNode } from 'react';
import './../../styles/layout/page-layout.less'; import './../../styles/layout/page-layout.less';
interface PageLayoutProp extends HTMLAttributes<HTMLDivElement> { interface PageLayoutProp extends HTMLAttributes<HTMLDivElement> {
@ -21,6 +22,7 @@ interface PageLayoutProp extends HTMLAttributes<HTMLDivElement> {
header?: ReactNode; header?: ReactNode;
rightPanel?: ReactNode; rightPanel?: ReactNode;
center?: boolean; center?: boolean;
pageTitle: string;
} }
export const pageContainerStyles = { export const pageContainerStyles = {
@ -35,49 +37,53 @@ const PageLayoutV1: FC<PageLayoutProp> = ({
children, children,
rightPanel, rightPanel,
className, className,
pageTitle,
center = false, center = false,
}: PageLayoutProp) => { }: PageLayoutProp) => {
return ( return (
<Row <Fragment>
className={className} <DocumentTitle title={pageTitle} />
data-testid="page-layout-v1" <Row
gutter={[16, 16]} className={className}
style={pageContainerStyles}> data-testid="page-layout-v1"
{leftPanel && ( gutter={[16, 16]}
<Col style={pageContainerStyles}>
className="page-layout-v1-vertical-scroll" {leftPanel && (
flex="284px" <Col
id="left-panelV1"> className="page-layout-v1-vertical-scroll"
{leftPanel} flex="284px"
</Col> id="left-panelV1">
)} {leftPanel}
<Col </Col>
className={classNames(
'page-layout-v1-center page-layout-v1-vertical-scroll',
{
'flex justify-center': center,
}
)} )}
flex={
leftPanel && rightPanel
? 'calc(100% - 568px)'
: leftPanel || rightPanel
? 'calc(100% - 284px)'
: '100%'
}
offset={center ? 3 : 0}
span={center ? 18 : 24}>
{children}
</Col>
{rightPanel && (
<Col <Col
className="page-layout-v1-vertical-scroll" className={classNames(
flex="284px" 'page-layout-v1-center page-layout-v1-vertical-scroll',
id="right-panelV1"> {
{rightPanel} 'flex justify-center': center,
}
)}
flex={
leftPanel && rightPanel
? 'calc(100% - 568px)'
: leftPanel || rightPanel
? 'calc(100% - 284px)'
: '100%'
}
offset={center ? 3 : 0}
span={center ? 18 : 24}>
{children}
</Col> </Col>
)} {rightPanel && (
</Row> <Col
className="page-layout-v1-vertical-scroll"
flex="284px"
id="right-panelV1">
{rightPanel}
</Col>
)}
</Row>
</Fragment>
); );
}; };

View File

@ -79,6 +79,7 @@
"basic-configuration": "Basic Configuration", "basic-configuration": "Basic Configuration",
"batch-size": "Batch Size", "batch-size": "Batch Size",
"bot": "Bot", "bot": "Bot",
"bot-detail": "Bot detail",
"bot-lowercase": "bot", "bot-lowercase": "bot",
"bot-plural": "Bots", "bot-plural": "Bots",
"broker-plural": "Brokers", "broker-plural": "Brokers",
@ -180,6 +181,7 @@
"data-insight-tier-summary": "Total Data Assets by Tier", "data-insight-tier-summary": "Total Data Assets by Tier",
"data-insight-top-viewed-entity-summary": "Most Viewed Data Assets", "data-insight-top-viewed-entity-summary": "Most Viewed Data Assets",
"data-insight-total-entity-summary": "Total Data Assets", "data-insight-total-entity-summary": "Total Data Assets",
"data-quality-test": "Data Quality Test",
"data-type": "Data Type", "data-type": "Data Type",
"database": "Database", "database": "Database",
"database-lowercase": "database", "database-lowercase": "database",
@ -755,6 +757,7 @@
"test-entity": "Test {{entity}}", "test-entity": "Test {{entity}}",
"test-plural": "Tests", "test-plural": "Tests",
"test-suite": "Test Suite", "test-suite": "Test Suite",
"test-suite-ingestion": "Test suite ingestion",
"test-suite-plural": "Test Suites", "test-suite-plural": "Test Suites",
"test-suite-status": "Test Suite Status", "test-suite-status": "Test Suite Status",
"test-type": "Test type", "test-type": "Test type",

View File

@ -233,7 +233,9 @@ const AddIngestionPage = () => {
} else { } else {
return ( return (
<div className="self-center"> <div className="self-center">
<PageLayoutV1 center> <PageLayoutV1
center
pageTitle={t('label.add-entity', { entity: t('label.ingestion') })}>
<Space direction="vertical" size="middle"> <Space direction="vertical" size="middle">
<TitleBreadcrumb titleLinks={slashedBreadcrumb} /> <TitleBreadcrumb titleLinks={slashedBreadcrumb} />
<div className="form-container"> <div className="form-container">

View File

@ -267,7 +267,9 @@ const DataInsightPage = () => {
}, [tab]); }, [tab]);
return ( return (
<PageLayoutV1 leftPanel={<DataInsightLeftPanel />}> <PageLayoutV1
leftPanel={<DataInsightLeftPanel />}
pageTitle={t('label.data-insight')}>
<Row data-testid="data-insight-container" gutter={[16, 16]}> <Row data-testid="data-insight-container" gutter={[16, 16]}>
<Col span={24}> <Col span={24}>
<Space className="w-full justify-between item-start"> <Space className="w-full justify-between item-start">

View File

@ -150,7 +150,7 @@ function EditConnectionFormPage() {
{getEntityMissingError(serviceCategory, serviceFQN)} {getEntityMissingError(serviceCategory, serviceFQN)}
</ErrorPlaceHolder> </ErrorPlaceHolder>
) : ( ) : (
<PageLayoutV1 center> <PageLayoutV1 center pageTitle={t('label.edit-connection')}>
<Space direction="vertical" size="middle"> <Space direction="vertical" size="middle">
<TitleBreadcrumb titleLinks={slashedBreadcrumb} /> <TitleBreadcrumb titleLinks={slashedBreadcrumb} />
<div className="form-container"> <div className="form-container">

View File

@ -23,6 +23,7 @@ import Loader from 'components/Loader/Loader';
import { startCase } from 'lodash'; import { startCase } from 'lodash';
import { ServicesUpdateRequest, ServiceTypes } from 'Models'; import { ServicesUpdateRequest, ServiceTypes } from 'Models';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory, useParams } from 'react-router-dom'; import { useHistory, useParams } from 'react-router-dom';
import { import {
deployIngestionPipelineById, deployIngestionPipelineById,
@ -60,6 +61,7 @@ import {
import { showErrorToast } from '../../utils/ToastUtils'; import { showErrorToast } from '../../utils/ToastUtils';
const EditIngestionPage = () => { const EditIngestionPage = () => {
const { t } = useTranslation();
const { isAirflowAvailable, fetchAirflowStatus } = useAirflowStatus(); const { isAirflowAvailable, fetchAirflowStatus } = useAirflowStatus();
const { ingestionFQN, ingestionType, serviceFQN, serviceCategory } = const { ingestionFQN, ingestionType, serviceFQN, serviceCategory } =
useParams<{ [key: string]: string }>(); useParams<{ [key: string]: string }>();
@ -265,7 +267,11 @@ const EditIngestionPage = () => {
} else { } else {
return ( return (
<div className="self-center"> <div className="self-center">
<PageLayoutV1 center> <PageLayoutV1
center
pageTitle={t('label.edit-entity', {
entity: t('label.ingestion'),
})}>
<Space direction="vertical" size="middle"> <Space direction="vertical" size="middle">
<TitleBreadcrumb titleLinks={slashedBreadcrumb} /> <TitleBreadcrumb titleLinks={slashedBreadcrumb} />
<div className="form-container"> <div className="form-container">

View File

@ -252,7 +252,9 @@ const GlossaryPage = () => {
return ( return (
<PageContainerV1> <PageContainerV1>
<PageLayoutV1 leftPanel={<GlossaryLeftPanel glossaries={glossaries} />}> <PageLayoutV1
leftPanel={<GlossaryLeftPanel glossaries={glossaries} />}
pageTitle={t('label.glossary')}>
{isRightPanelLoading ? ( {isRightPanelLoading ? (
// Loader for right panel data // Loader for right panel data
<Loader /> <Loader />

View File

@ -74,6 +74,16 @@ jest.mock('rest/glossaryAPI', () => ({
.mockImplementation(() => Promise.resolve({ data: MOCK_GLOSSARY })), .mockImplementation(() => Promise.resolve({ data: MOCK_GLOSSARY })),
})); }));
jest.mock('components/containers/PageLayoutV1', () =>
jest.fn().mockImplementation(({ children, leftPanel, rightPanel }) => (
<div>
{leftPanel}
{children}
{rightPanel}
</div>
))
);
describe('Test GlossaryComponent page', () => { describe('Test GlossaryComponent page', () => {
it('GlossaryComponent Page Should render', async () => { it('GlossaryComponent Page Should render', async () => {
render(<GlossaryPage />); render(<GlossaryPage />);

View File

@ -26,6 +26,7 @@ import {
LoadingNodeState, LoadingNodeState,
} from 'components/EntityLineage/EntityLineage.interface'; } from 'components/EntityLineage/EntityLineage.interface';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory, useParams } from 'react-router-dom'; import { useHistory, useParams } from 'react-router-dom';
import { getDashboardByFqn } from 'rest/dashboardAPI'; import { getDashboardByFqn } from 'rest/dashboardAPI';
import { getLineageByFQN } from 'rest/lineageAPI'; import { getLineageByFQN } from 'rest/lineageAPI';
@ -65,6 +66,7 @@ import { showErrorToast } from '../../utils/ToastUtils';
import './lineagePage.style.less'; import './lineagePage.style.less';
const LineagePage = () => { const LineagePage = () => {
const { t } = useTranslation();
const { entityType, entityFQN } = const { entityType, entityFQN } =
useParams<{ entityType: EntityType; entityFQN: string }>(); useParams<{ entityType: EntityType; entityFQN: string }>();
const history = useHistory(); const history = useHistory();
@ -336,7 +338,7 @@ const LineagePage = () => {
return ( return (
<PageContainerV1> <PageContainerV1>
<PageLayoutV1 className="p-x-lg"> <PageLayoutV1 className="p-x-lg" pageTitle={t('label.lineage')}>
<div className="lineage-page-container"> <div className="lineage-page-container">
<TitleBreadcrumb titleLinks={titleBreadcrumb} /> <TitleBreadcrumb titleLinks={titleBreadcrumb} />
<Card className="h-full" size="default"> <Card className="h-full" size="default">

View File

@ -44,6 +44,16 @@ jest.mock('../../../utils/ToastUtils', () => ({
showErrorToast: jest.fn(), showErrorToast: jest.fn(),
})); }));
jest.mock('components/containers/PageLayoutV1', () =>
jest.fn().mockImplementation(({ children, leftPanel, rightPanel }) => (
<div>
{leftPanel}
{children}
{rightPanel}
</div>
))
);
describe('Test Add Policy Page', () => { describe('Test Add Policy Page', () => {
it('Should Render the Add Policy page component', async () => { it('Should Render the Add Policy page component', async () => {
render(<AddPolicyPage />, { wrapper: MemoryRouter }); render(<AddPolicyPage />, { wrapper: MemoryRouter });

View File

@ -103,7 +103,9 @@ const AddPolicyPage = () => {
return ( return (
<div data-testid="add-policy-container"> <div data-testid="add-policy-container">
<PageLayoutV1 center> <PageLayoutV1
center
pageTitle={t('label.add-entity', { entity: t('label.policy') })}>
<Row> <Row>
<Col span={16}> <Col span={16}>
<Space className="w-full" direction="vertical" size="middle"> <Space className="w-full" direction="vertical" size="middle">

View File

@ -45,6 +45,16 @@ jest.mock('../../../utils/ToastUtils', () => ({
showErrorToast: jest.fn(), showErrorToast: jest.fn(),
})); }));
jest.mock('components/containers/PageLayoutV1', () =>
jest.fn().mockImplementation(({ children, leftPanel, rightPanel }) => (
<div>
{leftPanel}
{children}
{rightPanel}
</div>
))
);
describe('Test Add Role Page', () => { describe('Test Add Role Page', () => {
it('Should Render the Add Role page component', async () => { it('Should Render the Add Role page component', async () => {
render(<AddRolePage />, { wrapper: MemoryRouter }); render(<AddRolePage />, { wrapper: MemoryRouter });

View File

@ -98,7 +98,9 @@ const AddRolePage = () => {
return ( return (
<div data-testid="add-role-container"> <div data-testid="add-role-container">
<PageLayoutV1 center> <PageLayoutV1
center
pageTitle={t('label.add-entity', { entity: t('label.role') })}>
<Space direction="vertical" size="middle"> <Space direction="vertical" size="middle">
<TitleBreadcrumb titleLinks={breadcrumb} /> <TitleBreadcrumb titleLinks={breadcrumb} />
<Card> <Card>

View File

@ -18,6 +18,16 @@ const mockProps = {
children: <div data-testid="children" />, children: <div data-testid="children" />,
}; };
jest.mock('components/containers/PageLayoutV1', () =>
jest.fn().mockImplementation(({ children, leftPanel, rightPanel }) => (
<div>
{leftPanel}
{children}
{rightPanel}
</div>
))
);
describe('Test TaskPageLayout Component', () => { describe('Test TaskPageLayout Component', () => {
it('Should render the component', async () => { it('Should render the component', async () => {
render(<TaskPageLayout {...mockProps} />); render(<TaskPageLayout {...mockProps} />);

View File

@ -14,14 +14,19 @@
import PageContainerV1 from 'components/containers/PageContainerV1'; import PageContainerV1 from 'components/containers/PageContainerV1';
import PageLayoutV1 from 'components/containers/PageLayoutV1'; import PageLayoutV1 from 'components/containers/PageLayoutV1';
import React, { FC, HTMLAttributes } from 'react'; import React, { FC, HTMLAttributes } from 'react';
import { useTranslation } from 'react-i18next';
// eslint-disable-next-line @typescript-eslint/no-empty-interface // eslint-disable-next-line @typescript-eslint/no-empty-interface
interface Props extends HTMLAttributes<HTMLDivElement> {} interface Props extends HTMLAttributes<HTMLDivElement> {}
const TaskPageLayout: FC<Props> = ({ children }) => { const TaskPageLayout: FC<Props> = ({ children }) => {
const { t } = useTranslation();
return ( return (
<PageContainerV1> <PageContainerV1>
<PageLayoutV1 center>{children}</PageLayoutV1> <PageLayoutV1 center pageTitle={t('label.task')}>
{children}
</PageLayoutV1>
</PageContainerV1> </PageContainerV1>
); );
}; };

View File

@ -131,6 +131,7 @@ const TestSuiteIngestionPage = () => {
classes="tw-max-w-full-hd tw-h-full tw-pt-4" classes="tw-max-w-full-hd tw-h-full tw-pt-4"
header={<TitleBreadcrumb titleLinks={slashedBreadCrumb} />} header={<TitleBreadcrumb titleLinks={slashedBreadCrumb} />}
layout={PageLayoutType['2ColRTL']} layout={PageLayoutType['2ColRTL']}
pageTitle={t('label.test-suite-ingestion')}
rightPanel={<RightPanel data={INGESTION_DATA} />}> rightPanel={<RightPanel data={INGESTION_DATA} />}>
<TestSuiteIngestion <TestSuiteIngestion
ingestionPipeline={ingestionPipeline} ingestionPipeline={ingestionPipeline}

View File

@ -184,7 +184,7 @@ const TestSuitePage = () => {
} }
return ( return (
<PageLayoutV1> <PageLayoutV1 pageTitle={t('label.test-suite')}>
<Space align="center" className="w-full justify-between" size={16}> <Space align="center" className="w-full justify-between" size={16}>
<TitleBreadcrumb titleLinks={TEST_SUITE_BREADCRUMB} /> <TitleBreadcrumb titleLinks={TEST_SUITE_BREADCRUMB} />
<Tooltip <Tooltip

View File

@ -69,6 +69,16 @@ jest.mock(
} }
); );
jest.mock('components/containers/PageLayoutV1', () =>
jest.fn().mockImplementation(({ children, leftPanel, rightPanel }) => (
<div>
{leftPanel}
{children}
{rightPanel}
</div>
))
);
describe('Test Suite Stepper Page', () => { describe('Test Suite Stepper Page', () => {
it('Component should render', async () => { it('Component should render', async () => {
await act(async () => { await act(async () => {

View File

@ -86,7 +86,7 @@ const TestSuiteStepper = () => {
return ( return (
<div data-testid="test-suite-stepper-container"> <div data-testid="test-suite-stepper-container">
<PageLayoutV1 center> <PageLayoutV1 center pageTitle={t('label.test-suite')}>
<Space direction="vertical" size="middle"> <Space direction="vertical" size="middle">
<TitleBreadcrumb titleLinks={TEST_SUITE_STEPPER_BREADCRUMB} /> <TitleBreadcrumb titleLinks={TEST_SUITE_STEPPER_BREADCRUMB} />
{addIngestion ? ( {addIngestion ? (

View File

@ -773,7 +773,9 @@ const TagsPage = () => {
return ( return (
<PageContainerV1> <PageContainerV1>
<PageLayoutV1 leftPanel={fetchLeftPanel()}> <PageLayoutV1
leftPanel={fetchLeftPanel()}
pageTitle={t('label.tag-plural')}>
{isLoading ? ( {isLoading ? (
<Loader /> <Loader />
) : error ? ( ) : error ? (

View File

@ -12104,6 +12104,22 @@ react-error-boundary@^3.1.4:
dependencies: dependencies:
"@babel/runtime" "^7.12.5" "@babel/runtime" "^7.12.5"
react-fast-compare@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
react-helmet-async@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-1.3.0.tgz#7bd5bf8c5c69ea9f02f6083f14ce33ef545c222e"
integrity sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg==
dependencies:
"@babel/runtime" "^7.12.5"
invariant "^2.2.4"
prop-types "^15.7.2"
react-fast-compare "^3.2.0"
shallowequal "^1.1.0"
react-i18next@^11.18.6: react-i18next@^11.18.6:
version "11.18.6" version "11.18.6"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.18.6.tgz#e159c2960c718c1314f1e8fcaa282d1c8b167887" resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.18.6.tgz#e159c2960c718c1314f1e8fcaa282d1c8b167887"