[Stack Monitoring] Page layout fixes (#124182)

* adding EUI page layout components

* adding context menu button to toolbar and cleaning up tabs

* [Monitoring] Integrate setup mode toggle button with React tree

* Update unit tests

* Only show setup mode button on supported pages

* Move setup mode supported responsibility to renderer

* Fix failing unit test

Co-authored-by: Milton Hultgren <milton.hultgren@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Kate Farrar 2022-02-10 04:25:54 -07:00 committed by GitHub
parent c9118b4d41
commit aad83ed0b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 225 additions and 266 deletions

View file

@ -5,7 +5,15 @@
* 2.0.
*/
import { EuiTab, EuiTabs } from '@elastic/eui';
import {
EuiPage,
EuiPageContent,
EuiPageBody,
EuiPageContentBody,
EuiTab,
EuiTabs,
EuiSpacer,
} from '@elastic/eui';
import React, { useContext, useState, useEffect, useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import { IHttpFetchError, ResponseErrorBody } from 'kibana/public';
@ -16,11 +24,13 @@ import { PageLoading } from '../../components';
import {
getSetupModeState,
isSetupModeFeatureEnabled,
toggleSetupMode,
updateSetupModeData,
} from '../../lib/setup_mode';
import { SetupModeFeature } from '../../../common/enums';
import { AlertsDropdown } from '../../alerts/alerts_dropdown';
import { useRequestErrorHandler } from '../hooks/use_request_error_handler';
import { SetupModeToggleButton } from '../../components/setup_mode/toggle_button';
import { HeaderMenuPortal } from '../../../../observability/public';
import { HeaderActionMenuContext } from '../../application/contexts/header_action_menu_context';
@ -104,34 +114,44 @@ export const PageTemplate: React.FC<PageTemplateProps> = ({
return children;
};
const { supported, enabled } = getSetupModeState();
return (
<div className="app-container" data-test-subj="monitoringAppContainer">
{setHeaderActionMenu && theme$ && (
<HeaderMenuPortal setHeaderActionMenu={setHeaderActionMenu} theme$={theme$}>
<AlertsDropdown />
</HeaderMenuPortal>
)}
<MonitoringToolbar pageTitle={pageTitle} onRefresh={onRefresh} />
{tabs && (
<EuiTabs>
{tabs.map((item, idx) => {
return (
<EuiTab
key={idx}
disabled={isDisabledTab(product)}
title={item.label}
data-test-subj={item.testSubj}
href={createHref(item.route)}
isSelected={isTabSelected(item.route)}
>
{item.label}
</EuiTab>
);
})}
</EuiTabs>
)}
<div>{renderContent()}</div>
</div>
<EuiPage data-test-subj="monitoringAppContainer">
<EuiPageBody>
<EuiPageContent>
{setHeaderActionMenu && theme$ && (
<HeaderMenuPortal setHeaderActionMenu={setHeaderActionMenu} theme$={theme$}>
{supported && (
<SetupModeToggleButton enabled={enabled} toggleSetupMode={toggleSetupMode} />
)}
<AlertsDropdown />
</HeaderMenuPortal>
)}
<MonitoringToolbar pageTitle={pageTitle} onRefresh={onRefresh} />
<EuiSpacer size="m" />
{tabs && (
<EuiTabs size="l">
{tabs.map((item, idx) => {
return (
<EuiTab
key={idx}
disabled={isDisabledTab(product)}
title={item.label}
data-test-subj={item.testSubj}
href={createHref(item.route)}
isSelected={isTabSelected(item.route)}
>
{item.label}
</EuiTab>
);
})}
</EuiTabs>
)}
<EuiPageContentBody>{renderContent()}</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
);
};

View file

@ -76,17 +76,9 @@ exports[`SetupModeRenderer should render the flyout open 1`] = `
<EuiFlexItem
grow={false}
>
<EuiButton
color="danger"
data-test-subj="exitSetupModeBtn"
fill={true}
iconSide="right"
iconType="flag"
onClick={[Function]}
size="s"
>
Exit setup mode
</EuiButton>
<SetupModeExitButton
exitSetupMode={[Function]}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
@ -177,17 +169,9 @@ exports[`SetupModeRenderer should render with setup mode enabled 1`] = `
<EuiFlexItem
grow={false}
>
<EuiButton
color="danger"
data-test-subj="exitSetupModeBtn"
fill={true}
iconSide="right"
iconType="flag"
onClick={[Function]}
size="s"
>
Exit setup mode
</EuiButton>
<SetupModeExitButton
exitSetupMode={[Function]}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>

View file

@ -5,31 +5,31 @@
* 2.0.
*/
import React, { Fragment } from 'react';
import {
getSetupModeState,
initSetupModeState,
updateSetupModeData,
disableElasticsearchInternalCollection,
toggleSetupMode,
setSetupModeMenuItem,
} from '../../lib/setup_mode';
import { Flyout } from '../metricbeat_migration/flyout';
import {
EuiBottomBar,
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiTextColor,
EuiIcon,
EuiSpacer,
EuiTextColor,
} from '@elastic/eui';
import { findNewUuid } from './lib/find_new_uuid';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { GlobalStateContext } from '../../application/contexts/global_state_context';
import React, { Fragment } from 'react';
import { withKibana } from '../../../../../../src/plugins/kibana_react/public';
import { GlobalStateContext } from '../../application/contexts/global_state_context';
import { useRequestErrorHandler } from '../../application/hooks/use_request_error_handler';
import {
disableElasticsearchInternalCollection,
getSetupModeState,
initSetupModeState,
markSetupModeSupported,
markSetupModeUnsupported,
toggleSetupMode,
updateSetupModeData,
} from '../../lib/setup_mode';
import { Flyout } from '../metricbeat_migration/flyout';
import { SetupModeExitButton } from '../setup_mode/exit_button';
import { findNewUuid } from './lib/find_new_uuid';
export class WrappedSetupModeRenderer extends React.Component {
globalState;
@ -44,6 +44,7 @@ export class WrappedSetupModeRenderer extends React.Component {
UNSAFE_componentWillMount() {
this.globalState = this.context;
const { kibana, onHttpError } = this.props;
markSetupModeSupported();
initSetupModeState(this.globalState, kibana.services.http, onHttpError, (_oldData) => {
const newState = { renderState: true };
@ -70,7 +71,10 @@ export class WrappedSetupModeRenderer extends React.Component {
this.setState(newState);
});
setSetupModeMenuItem();
}
componentWillUnmount() {
markSetupModeUnsupported();
}
reset() {
@ -155,19 +159,7 @@ export class WrappedSetupModeRenderer extends React.Component {
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButton
color="danger"
fill
iconType="flag"
iconSide="right"
size="s"
onClick={() => toggleSetupMode(false)}
data-test-subj="exitSetupModeBtn"
>
{i18n.translate('xpack.monitoring.setupMode.exit', {
defaultMessage: `Exit setup mode`,
})}
</EuiButton>
<SetupModeExitButton exitSetupMode={() => toggleSetupMode(false)} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>

View file

@ -23,11 +23,13 @@ describe('SetupModeRenderer', () => {
it('should render with setup mode disabled', () => {
jest.doMock('../../lib/setup_mode', () => ({
getSetupModeState: () => ({
supported: true,
enabled: false,
}),
initSetupModeState: () => {},
updateSetupModeData: () => {},
setSetupModeMenuItem: () => {},
markSetupModeSupported: () => {},
markSetupModeUnsupported: () => {},
}));
const SetupModeRenderer = require('./setup_mode').WrappedSetupModeRenderer;
@ -53,6 +55,7 @@ describe('SetupModeRenderer', () => {
it('should render with setup mode enabled', () => {
jest.doMock('../../lib/setup_mode', () => ({
getSetupModeState: () => ({
supported: true,
enabled: true,
data: {
elasticsearch: {},
@ -61,7 +64,8 @@ describe('SetupModeRenderer', () => {
}),
initSetupModeState: () => {},
updateSetupModeData: () => {},
setSetupModeMenuItem: () => {},
markSetupModeSupported: () => {},
markSetupModeUnsupported: () => {},
}));
const SetupModeRenderer = require('./setup_mode').WrappedSetupModeRenderer;
@ -87,6 +91,7 @@ describe('SetupModeRenderer', () => {
it('should render the flyout open', () => {
jest.doMock('../../lib/setup_mode', () => ({
getSetupModeState: () => ({
supported: true,
enabled: true,
data: {
elasticsearch: {
@ -97,7 +102,8 @@ describe('SetupModeRenderer', () => {
}),
initSetupModeState: () => {},
updateSetupModeData: () => {},
setSetupModeMenuItem: () => {},
markSetupModeSupported: () => {},
markSetupModeUnsupported: () => {},
}));
const SetupModeRenderer = require('./setup_mode').WrappedSetupModeRenderer;
@ -125,6 +131,7 @@ describe('SetupModeRenderer', () => {
it('should handle a new node/instance scenario', () => {
jest.doMock('../../lib/setup_mode', () => ({
getSetupModeState: () => ({
supported: true,
enabled: true,
data: {
elasticsearch: {
@ -135,7 +142,8 @@ describe('SetupModeRenderer', () => {
}),
initSetupModeState: () => {},
updateSetupModeData: () => {},
setSetupModeMenuItem: () => {},
markSetupModeSupported: () => {},
markSetupModeUnsupported: () => {},
}));
const SetupModeRenderer = require('./setup_mode').WrappedSetupModeRenderer;
@ -166,6 +174,7 @@ describe('SetupModeRenderer', () => {
jest.useFakeTimers();
jest.doMock('../../lib/setup_mode', () => ({
getSetupModeState: () => ({
supported: true,
enabled: true,
data: {
elasticsearch: {
@ -188,7 +197,8 @@ describe('SetupModeRenderer', () => {
}, 500);
},
updateSetupModeData: () => {},
setSetupModeMenuItem: () => {},
markSetupModeSupported: () => {},
markSetupModeUnsupported: () => {},
}));
const SetupModeRenderer = require('./setup_mode').WrappedSetupModeRenderer;
@ -220,9 +230,9 @@ describe('SetupModeRenderer', () => {
it('should set the top menu items', () => {
const newProduct = { id: 1 };
const setSetupModeMenuItem = jest.fn();
jest.doMock('../../lib/setup_mode', () => ({
getSetupModeState: () => ({
supported: true,
enabled: true,
data: {
elasticsearch: {
@ -245,7 +255,8 @@ describe('SetupModeRenderer', () => {
}, 500);
},
updateSetupModeData: () => {},
setSetupModeMenuItem,
markSetupModeSupported: () => {},
markSetupModeUnsupported: () => {},
}));
const SetupModeRenderer = require('./setup_mode').WrappedSetupModeRenderer;
@ -267,6 +278,5 @@ describe('SetupModeRenderer', () => {
component.setState({ isFlyoutOpen: true });
component.update();
expect(setSetupModeMenuItem).toHaveBeenCalled();
});
});

View file

@ -1,18 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EnterButton should render properly 1`] = `
<div
className="monSetupModeEnterButton__buttonWrapper"
>
<EuiButton
data-test-subj="monitoringSetupModeBtn"
iconSide="right"
iconType="flag"
isLoading={false}
onClick={[Function]}
size="s"
>
Enter setup mode
</EuiButton>
</div>
`;

View file

@ -1,3 +0,0 @@
.monSetupModeEnterButton__buttonWrapper {
padding: $euiSizeM;
}

View file

@ -1,43 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { SetupModeEnterButton } from './enter_button';
describe('EnterButton', () => {
it('should render properly', () => {
const component = shallow(<SetupModeEnterButton enabled={true} toggleSetupMode={jest.fn()} />);
expect(component).toMatchSnapshot();
});
it('should show a loading state', () => {
const component = shallow(<SetupModeEnterButton enabled={true} toggleSetupMode={jest.fn()} />);
component.find('EuiButton').simulate('click');
expect(component.find('EuiButton').prop('isLoading')).toBe(true);
});
it('should call toggleSetupMode', () => {
const toggleSetupMode = jest.fn();
const component = shallow(
<SetupModeEnterButton enabled={true} toggleSetupMode={toggleSetupMode} />
);
component.find('EuiButton').simulate('click');
expect(toggleSetupMode).toHaveBeenCalledWith(true);
});
it('should not render if not enabled', () => {
const toggleSetupMode = jest.fn();
const component = shallow(
<SetupModeEnterButton enabled={false} toggleSetupMode={toggleSetupMode} />
);
expect(component.html()).toBe(null);
});
});

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { EuiButton } from '@elastic/eui';
import React from 'react';
interface SetupModeExitButtonProps {
exitSetupMode: () => void;
}
export function SetupModeExitButton({ exitSetupMode }: SetupModeExitButtonProps) {
return (
<EuiButton
color="danger"
fill
iconType="flag"
iconSide="right"
size="s"
onClick={exitSetupMode}
data-test-subj="exitSetupModeBtn"
>
{i18n.translate('xpack.monitoring.setupMode.exit', {
defaultMessage: `Exit setup mode`,
})}
</EuiButton>
);
}

View file

@ -0,0 +1,52 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { act, fireEvent, render } from '@testing-library/react';
import { SetupModeToggleButton } from './toggle_button';
describe('ToggleButton', () => {
describe('when setup mode is disabled', () => {
it('should render the enter setup mode button', () => {
const { getByText } = render(
<SetupModeToggleButton enabled={false} toggleSetupMode={jest.fn()} />
);
expect(getByText('Enter setup mode')).toBeInTheDocument();
});
it('should call toggleSetupMode to enable setup mode', () => {
const toggleSetupMode = jest.fn();
const { getByText } = render(
<SetupModeToggleButton enabled={false} toggleSetupMode={toggleSetupMode} />
);
act(() => {
fireEvent.click(getByText('Enter setup mode'));
});
expect(toggleSetupMode).toHaveBeenCalledWith(true);
});
});
describe('when setup mode is enabled', () => {
it('should render the exit setup mode button', () => {
const { getByText } = render(
<SetupModeToggleButton enabled={true} toggleSetupMode={jest.fn()} />
);
expect(getByText('Exit setup mode')).toBeInTheDocument();
});
it('should call toggleSetupMode to disable setup mode', () => {
const toggleSetupMode = jest.fn();
const { getByText } = render(
<SetupModeToggleButton enabled={true} toggleSetupMode={toggleSetupMode} />
);
act(() => {
fireEvent.click(getByText('Exit setup mode'));
});
expect(toggleSetupMode).toHaveBeenCalledWith(false);
});
});
});

View file

@ -8,49 +8,47 @@
import React from 'react';
import { EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import './enter_button.scss';
import { METRIC_TYPE, useUiTracker } from '../../../../observability/public';
import { TELEMETRY_METRIC_BUTTON_CLICK } from '../../../common/constants';
import { SetupModeExitButton } from './exit_button';
export interface SetupModeEnterButtonProps {
export interface SetupModeToggleButtonProps {
enabled: boolean;
toggleSetupMode: (state: boolean) => void;
}
export const SetupModeEnterButton: React.FC<SetupModeEnterButtonProps> = (
props: SetupModeEnterButtonProps
export const SetupModeToggleButton: React.FC<SetupModeToggleButtonProps> = (
props: SetupModeToggleButtonProps
) => {
const [isLoading, setIsLoading] = React.useState(false);
const trackStat = useUiTracker({ app: 'stack_monitoring' });
if (!props.enabled) {
return null;
}
async function enterSetupMode() {
function toggleSetupMode(enabled: boolean, stat: string) {
setIsLoading(true);
await props.toggleSetupMode(true);
props.toggleSetupMode(enabled);
trackStat({
metric: `${TELEMETRY_METRIC_BUTTON_CLICK}setupmode_enter`,
metric: `${TELEMETRY_METRIC_BUTTON_CLICK}setupmode_${stat}`,
metricType: METRIC_TYPE.CLICK,
});
setIsLoading(false);
}
if (props.enabled) {
return <SetupModeExitButton exitSetupMode={() => toggleSetupMode(false, 'exit')} />;
}
return (
<div className="monSetupModeEnterButton__buttonWrapper">
<EuiButton
onClick={enterSetupMode}
iconType="flag"
size="s"
iconSide="right"
isLoading={isLoading}
data-test-subj="monitoringSetupModeBtn"
>
{i18n.translate('xpack.monitoring.setupMode.enter', {
defaultMessage: 'Enter setup mode',
})}
</EuiButton>
</div>
<EuiButton
onClick={() => toggleSetupMode(true, 'enter')}
iconType="flag"
size="s"
iconSide="right"
isLoading={isLoading}
data-test-subj="monitoringSetupModeBtn"
>
{i18n.translate('xpack.monitoring.setupMode.enter', {
defaultMessage: 'Enter setup mode',
})}
</EuiButton>
);
};

View file

@ -5,13 +5,7 @@
* 2.0.
*/
import {
EuiFlexGroup,
EuiFlexItem,
EuiSuperDatePicker,
EuiTitle,
OnRefreshChangeProps,
} from '@elastic/eui';
import { EuiPageHeader, EuiSuperDatePicker, OnRefreshChangeProps } from '@elastic/eui';
import React, { useContext, useCallback, useMemo } from 'react';
import { MonitoringTimeContainer } from '../../application/hooks/use_monitoring_time';
import { GlobalStateContext } from '../../application/contexts/global_state_context';
@ -83,39 +77,21 @@ export const MonitoringToolbar: React.FC<MonitoringToolbarProps> = ({ pageTitle,
);
return (
<EuiFlexGroup gutterSize="l" justifyContent="spaceBetween" responsive>
<EuiFlexItem>
<EuiFlexGroup gutterSize="none" justifyContent="spaceEvenly" direction="column" responsive>
<EuiFlexItem>
<div id="setupModeNav">{/* HERE GOES THE SETUP BUTTON */}</div>
</EuiFlexItem>
<EuiFlexItem className="monTopNavSecondItem">
{pageTitle && (
<div data-test-subj="monitoringPageTitle">
<EuiTitle size="xs">
<h1>{pageTitle}</h1>
</EuiTitle>
</div>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<div style={{ padding: 8 }}>
<EuiSuperDatePicker
isDisabled={isDisabled}
start={currentTimerange.from}
end={currentTimerange.to}
onTimeChange={onTimeChange}
onRefresh={onRefresh}
isPaused={isPaused}
refreshInterval={refreshInterval}
onRefreshChange={onRefreshChange}
commonlyUsedRanges={commonlyUsedRanges}
/>
</div>
</EuiFlexItem>
</EuiFlexGroup>
<EuiPageHeader
pageTitle={pageTitle}
rightSideItems={[
<EuiSuperDatePicker
isDisabled={isDisabled}
start={currentTimerange.from}
end={currentTimerange.to}
onTimeChange={onTimeChange}
onRefresh={onRefresh}
isPaused={isPaused}
refreshInterval={refreshInterval}
onRefreshChange={onRefreshChange}
commonlyUsedRanges={commonlyUsedRanges}
/>,
]}
/>
);
};

View file

@ -9,15 +9,10 @@ let toggleSetupMode;
let initSetupModeState;
let getSetupModeState;
let updateSetupModeData;
let setSetupModeMenuItem;
const handleErrorsMock = jest.fn();
const callbackMock = jest.fn();
jest.mock('react-dom', () => ({
render: jest.fn(),
}));
jest.mock('../legacy_shims', () => {
return {
Legacy: {
@ -39,7 +34,6 @@ function setModulesAndMocks() {
initSetupModeState = setupMode.initSetupModeState;
getSetupModeState = setupMode.getSetupModeState;
updateSetupModeData = setupMode.updateSetupModeData;
setSetupModeMenuItem = setupMode.setSetupModeMenuItem;
}
function waitForSetupModeData() {
@ -80,24 +74,6 @@ describe('setup_mode', () => {
toggleSetupMode(false);
expect(globalState.inSetupMode).toBe(false);
});
it('should set top nav config', async () => {
const globalState = {
inSetupMode: false,
save: jest.fn(),
};
const httpServiceMock = {
post: jest.fn(),
};
const render = require('react-dom').render;
await initSetupModeState(globalState, httpServiceMock, handleErrorsMock, callbackMock);
setSetupModeMenuItem();
toggleSetupMode(true);
expect(render.mock.calls.length).toBe(2);
});
});
describe('in setup mode', () => {

View file

@ -5,33 +5,27 @@
* 2.0.
*/
import React from 'react';
import { render } from 'react-dom';
import { get, includes } from 'lodash';
import { get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { HttpStart, IHttpFetchError, ResponseErrorBody } from 'kibana/public';
import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public';
import { Legacy } from '../legacy_shims';
import { SetupModeEnterButton } from '../components/setup_mode/enter_button';
import { SetupModeFeature } from '../../common/enums';
import { ISetupModeContext } from '../components/setup_mode/setup_mode_context';
import { State as GlobalState } from '../application/contexts/global_state_context';
function isOnPage(hash: string) {
return includes(window.location.hash, hash);
}
let globalState: GlobalState;
let httpService: HttpStart;
let errorHandler: (error: IHttpFetchError<ResponseErrorBody>) => void;
interface ISetupModeState {
supported: boolean;
enabled: boolean;
data: any;
callback?: (() => void) | null;
hideBottomBar: boolean;
}
const setupModeState: ISetupModeState = {
supported: false,
enabled: false,
data: null,
callback: null,
@ -132,7 +126,6 @@ export const toggleSetupMode = (inSetupMode: boolean) => {
setupModeState.enabled = inSetupMode;
globalState.inSetupMode = inSetupMode;
globalState.save?.();
setSetupModeMenuItem();
notifySetupModeDataChange();
if (inSetupMode) {
@ -141,22 +134,12 @@ export const toggleSetupMode = (inSetupMode: boolean) => {
}
};
export const setSetupModeMenuItem = () => {
if (isOnPage('no-data')) {
return;
}
export const markSetupModeSupported = () => {
setupModeState.supported = true;
};
const enabled = !globalState.inSetupMode;
const I18nContext = Legacy.shims.I18nContext;
render(
<KibanaContextProvider services={Legacy.shims.kibanaServices}>
<I18nContext>
<SetupModeEnterButton enabled={enabled} toggleSetupMode={toggleSetupMode} />
</I18nContext>
</KibanaContextProvider>,
document.getElementById('setupModeNav')
);
export const markSetupModeUnsupported = () => {
setupModeState.supported = false;
};
export const initSetupModeState = async (