Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Backport 2.x] Support new page header #354

Merged
merged 1 commit into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions public/application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const renderApp = (
setActionMenu={setHeaderActionMenu}
dataSource={services.dataSource}
dataSourceManagement={services.dataSourceManagement}
application={services.application}
/>
</services.i18n.Context>
</OpenSearchDashboardsContextProvider>
Expand Down
19 changes: 17 additions & 2 deletions public/components/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import React from 'react';
import { I18nProvider } from '@osd/i18n/react';
import { Redirect, Route, Switch } from 'react-router-dom';
import { EuiPage, EuiPageBody } from '@elastic/eui';
import { useObservable } from 'react-use';
import { ROUTES } from '../../common/router';
import { routerPaths } from '../../common/router_paths';

Expand Down Expand Up @@ -37,6 +38,7 @@ interface MlCommonsPluginAppDeps {
dataSource?: DataSourcePluginSetup;
dataSourceManagement?: DataSourceManagementPluginSetup;
setActionMenu: (menuMount: MountPoint | undefined) => void;
application: CoreStart['application'];
}

export interface ComponentsCommonProps {
Expand All @@ -55,8 +57,12 @@ export const MlCommonsPluginApp = ({
dataSourceManagement,
savedObjects,
setActionMenu,
navigation,
uiSettingsClient,
application,
}: MlCommonsPluginAppDeps) => {
const dataSourceEnabled = !!dataSource;
const useNewPageHeader = useObservable(uiSettingsClient.get$('home:useNewHomePage'));
return (
<I18nProvider>
<DataSourceContextProvider
Expand All @@ -73,7 +79,15 @@ export const MlCommonsPluginApp = ({
key={path}
path={path}
render={() => (
<Component http={http} notifications={notifications} data={data} />
<Component
http={http}
notifications={notifications}
chrome={chrome}
data={data}
navigation={navigation}
useNewPageHeader={useNewPageHeader}
application={application}
/>
)}
exact={exact ?? false}
/>
Expand All @@ -82,7 +96,8 @@ export const MlCommonsPluginApp = ({
</Switch>
</EuiPageBody>
</EuiPage>
<GlobalBreadcrumbs chrome={chrome} basename={basename} />
{/* Breadcrumbs will contains dynamic content in new page header, should be provided by each page self*/}
{!useNewPageHeader && <GlobalBreadcrumbs chrome={chrome} basename={basename} />}
{dataSourceEnabled && (
<DataSourceTopNavMenu
notifications={notifications}
Expand Down
24 changes: 22 additions & 2 deletions public/components/monitoring/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { render, screen, waitFor, within } from '../../../../test/test_utils';
import { Monitoring } from '../index';
import * as useMonitoringExports from '../use_monitoring';
import { APIProvider } from '../../../apis/api_provider';
import { applicationServiceMock, chromeServiceMock } from '../../../../../../src/core/public/mocks';
import { navigationPluginMock } from '../../../../../../src/plugins/navigation/public/mocks';

jest.mock('../../../../../../src/plugins/opensearch_dashboards_react/public', () => {
return {
Expand All @@ -18,7 +20,8 @@ jest.mock('../../../../../../src/plugins/opensearch_dashboards_react/public', ()
});

const setup = (
monitoringReturnValue?: Partial<ReturnType<typeof useMonitoringExports.useMonitoring>>
monitoringReturnValue?: Partial<ReturnType<typeof useMonitoringExports.useMonitoring>>,
useNewPageHeader = false
) => {
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
const finalMonitoringReturnValue = {
Expand Down Expand Up @@ -85,7 +88,18 @@ const setup = (
...monitoringReturnValue,
} as ReturnType<typeof useMonitoringExports.useMonitoring>;
jest.spyOn(useMonitoringExports, 'useMonitoring').mockReturnValue(finalMonitoringReturnValue);
render(<Monitoring />);
const applicationStartMock = applicationServiceMock.createStartContract();
const chromeStartMock = chromeServiceMock.createStartContract();
const navigationStartMock = navigationPluginMock.createStartContract();
navigationStartMock.ui.HeaderControl = () => null;
render(
<Monitoring
application={applicationStartMock}
chrome={chromeStartMock}
navigation={navigationStartMock}
useNewPageHeader={useNewPageHeader}
/>
);
return { finalMonitoringReturnValue, user };
};

Expand Down Expand Up @@ -382,4 +396,10 @@ describe('<Monitoring />', () => {
await user.click(screen.getByLabelText('Close this dialog'));
expect(reload).not.toHaveBeenCalled();
});

it('should NOT render table header title if useNewPageHeader equal true', () => {
setup({}, true);

expect(screen.queryByLabelText('total number of results')).toBe(null);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import userEvent from '@testing-library/user-event';
import { applicationServiceMock } from '../../../../../../src/core/public/mocks';
import { navigationPluginMock } from '../../../../../../src/plugins/navigation/public/mocks';

import { render, screen } from '../../../../test/test_utils';
import { MonitoringPageHeader, MonitoringPageHeaderProps } from '../monitoring_page_header';

jest.mock('../../../apis/connector');

async function setup(options: Partial<MonitoringPageHeaderProps>) {
const setBreadcrumbsMock = jest.fn();
const onRefreshMock = jest.fn();
const applicationStartMock = applicationServiceMock.createStartContract();
const navigationStartMock = navigationPluginMock.createStartContract();
const user = userEvent.setup({});

navigationStartMock.ui.HeaderControl = ({ controls }) => {
return controls?.[0].renderComponent ?? null;
};

const renderResult = render(
<MonitoringPageHeader
navigation={navigationStartMock}
application={applicationStartMock}
setBreadcrumbs={setBreadcrumbsMock}
onRefresh={onRefreshMock}
useNewPageHeader={true}
{...options}
/>
);

return {
user,
renderResult,
setBreadcrumbsMock,
onRefreshMock,
applicationStartMock,
navigationStartMock,
};
}

describe('<MonitoringPageHeader />', () => {
it('should old page header and refresh button when usePageHeader is false', async () => {
await setup({
useNewPageHeader: false,
});
expect(screen.getByText('Overview')).toBeInTheDocument();
expect(screen.getByLabelText('set refresh interval')).toBeInTheDocument();
});

it('should set breadcrumbs and render refresh button', async () => {
const { setBreadcrumbsMock } = await setup({
useNewPageHeader: true,
recordsCount: 2,
});

expect(setBreadcrumbsMock).toHaveBeenCalledWith([
{
text: 'AI models (2)',
},
]);
expect(screen.getByLabelText('set refresh interval')).toBeInTheDocument();
});
});
61 changes: 40 additions & 21 deletions public/components/monitoring/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import {
EuiPanel,
EuiPageHeader,
EuiSpacer,
EuiTextColor,
EuiFlexGroup,
Expand All @@ -14,19 +13,30 @@ import {
EuiFilterGroup,
} from '@elastic/eui';
import React, { useState, useRef, useCallback } from 'react';
import { FormattedMessage } from '@osd/i18n/react';

import { ModelDeploymentProfile } from '../../apis/profile';
import { RefreshInterval } from '../common/refresh_interval';
import { PreviewPanel } from '../preview_panel';
import { ApplicationStart, ChromeStart } from '../../../../../src/core/public';
import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public';

import { ModelDeploymentItem, ModelDeploymentTable } from './model_deployment_table';
import { useMonitoring } from './use_monitoring';
import { ModelStatusFilter } from './model_status_filter';
import { SearchBar } from './search_bar';
import { ModelSourceFilter } from './model_source_filter';
import { ModelConnectorFilter } from './model_connector_filter';
import { MonitoringPageHeader } from './monitoring_page_header';

export const Monitoring = () => {
interface MonitoringProps {
chrome: ChromeStart;
navigation: NavigationPublicPluginStart;
application: ApplicationStart;
useNewPageHeader: boolean;
}

export const Monitoring = (props: MonitoringProps) => {
const { useNewPageHeader, chrome, application, navigation } = props;
const {
pageStatus,
params,
Expand Down Expand Up @@ -83,25 +93,34 @@ export const Monitoring = () => {
);

return (
<div>
<EuiSpacer size="s" />
<EuiSpacer size="xs" />
<EuiPageHeader
pageTitle="Overview"
rightSideItems={[<RefreshInterval onRefresh={reload} />]}
<>
<MonitoringPageHeader
onRefresh={reload}
navigation={navigation}
setBreadcrumbs={chrome.setBreadcrumbs}
recordsCount={pagination?.totalRecords}
application={application}
useNewPageHeader={useNewPageHeader}
/>
<EuiSpacer size="m" />
<EuiPanel>
<EuiText size="s">
<h2>
Models{' '}
{pageStatus !== 'empty' && (
<EuiTextColor aria-label="total number of results" color="subdued">
({pagination?.totalRecords ?? 0})
</EuiTextColor>
)}
</h2>
</EuiText>
{!useNewPageHeader && (
<EuiText size="s">
<h2>
<FormattedMessage
id="machineLearning.aiModels.table.header.title"
defaultMessage="Models {records}"
values={{
records:
pageStatus === 'normal' ? (
<EuiTextColor aria-label="total number of results" color="subdued">
({pagination?.totalRecords ?? 0})
</EuiTextColor>
) : undefined,
}}
/>
</h2>
</EuiText>
)}

<EuiSpacer size="m" />
{pageStatus !== 'empty' && (
Expand Down Expand Up @@ -145,6 +164,6 @@ export const Monitoring = () => {
/>
)}
</EuiPanel>
</div>
</>
);
};
75 changes: 75 additions & 0 deletions public/components/monitoring/monitoring_page_header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
import React, { useEffect, useMemo } from 'react';
import { EuiPageHeader, EuiSpacer } from '@elastic/eui';
import { i18n } from '@osd/i18n';

import type { ChromeBreadcrumb } from 'opensearch-dashboards/public';

import { RefreshInterval } from '../common/refresh_interval';
import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public';
import { ApplicationStart } from '../../../../../src/core/public';

export interface MonitoringPageHeaderProps {
navigation: NavigationPublicPluginStart;
application: ApplicationStart;
onRefresh: () => void;
recordsCount?: number;
setBreadcrumbs: (breadcrumbs: ChromeBreadcrumb[]) => void;
useNewPageHeader: boolean;
}

export const MonitoringPageHeader = ({
onRefresh,
navigation,
recordsCount,
setBreadcrumbs,
application,
useNewPageHeader,
}: MonitoringPageHeaderProps) => {
const { HeaderControl } = navigation.ui;
const { setAppRightControls } = application;
const controls = useMemo(() => {
if (useNewPageHeader) {
return [
{
renderComponent: <RefreshInterval onRefresh={onRefresh} />,
},
];
}
return [];
}, [useNewPageHeader, onRefresh]);

useEffect(() => {
if (useNewPageHeader) {
setBreadcrumbs([
{
text: i18n.translate('machineLearning.AIModels.page.title', {
defaultMessage:
'AI models {recordsCount, select, undefined {} other {({recordsCount})}}',
values: {
recordsCount,
},
}),
},
]);
}
}, [useNewPageHeader, recordsCount, setBreadcrumbs]);

if (useNewPageHeader) {
return <HeaderControl setMountPoint={setAppRightControls} controls={controls} />;
}
return (
<>
<EuiSpacer size="s" />
<EuiSpacer size="xs" />
<EuiPageHeader
pageTitle="Overview"
rightSideItems={[<RefreshInterval onRefresh={onRefresh} />]}
/>
<EuiSpacer size="m" />
</>
);
};
Loading