[ML] Fix responsive behaviour of page header with date picker (#149073)

Improve responsive behaviour of page header with date picker.
- Removes custom breakpoint based code to determine date picker width
and instead use it's native `width` option.
- Adds a `flexGroup` boolean toggle to be able to get back the flex
items only without the flex group if you want to embed the date picker
in an already existing flex group to avoid additional nesting.
- Sets the `fill` option of the refresh button to `false` to avoid the
dark blue "primary".
- In the `aiops` plugin and the `ml_page.tsx` component, migrates away
from EUI's deprecated components for the page layout.
- Adds a `min-width` to page titles to avoid narrow wrapping (e.g.
wrapping after each character on narrow screens).
This commit is contained in:
Walter Rafelsberger 2023-01-23 13:55:29 +01:00 committed by GitHub
parent 99e3810e0c
commit 2a57862668
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 153 additions and 142 deletions

View file

@ -5,18 +5,17 @@
* 2.0.
*/
import { css } from '@emotion/react';
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { Subscription } from 'rxjs';
import { debounce } from 'lodash';
import {
useEuiBreakpoint,
useIsWithinMaxBreakpoint,
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiSuperDatePicker,
type EuiSuperDatePickerProps,
OnRefreshProps,
OnTimeChangeProps,
} from '@elastic/eui';
@ -32,7 +31,6 @@ import { useDatePickerContext } from '../hooks/use_date_picker_context';
import { mlTimefilterRefresh$ } from '../services/timefilter_refresh_service';
const DEFAULT_REFRESH_INTERVAL_MS = 5000;
const DATE_PICKER_MAX_WIDTH = '540px';
interface TimePickerQuickRange {
from: string;
@ -83,6 +81,14 @@ interface DatePickerWrapperProps {
* Boolean flag to enforce showing/hiding the refresh button.
*/
showRefresh?: boolean;
/**
* Width setting to be passed on to `EuiSuperDatePicker`
*/
width?: EuiSuperDatePickerProps['width'];
/**
* Boolean flag to set use of flex group wrapper
*/
flexGroup?: boolean;
}
/**
@ -93,7 +99,7 @@ interface DatePickerWrapperProps {
* @returns {React.ReactElement} The DatePickerWrapper component.
*/
export const DatePickerWrapper: FC<DatePickerWrapperProps> = (props) => {
const { isAutoRefreshOnly, isLoading = false, showRefresh } = props;
const { isAutoRefreshOnly, isLoading = false, showRefresh, width, flexGroup = true } = props;
const {
data,
notifications: { toasts },
@ -274,15 +280,9 @@ export const DatePickerWrapper: FC<DatePickerWrapperProps> = (props) => {
setRefreshInterval({ pause, value });
}
const datePickerWidth = css({
[useEuiBreakpoint(['xs', 's', 'm', 'l'])]: {
maxWidth: DATE_PICKER_MAX_WIDTH,
},
});
return isAutoRefreshSelectorEnabled || isTimeRangeSelectorEnabled ? (
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false} css={datePickerWidth}>
const flexItems = (
<>
<EuiFlexItem>
<EuiSuperDatePicker
isLoading={isLoading}
start={time.from}
@ -296,14 +296,14 @@ export const DatePickerWrapper: FC<DatePickerWrapperProps> = (props) => {
recentlyUsedRanges={recentlyUsedRanges}
dateFormat={dateFormat}
commonlyUsedRanges={commonlyUsedRanges}
updateButtonProps={{ iconOnly: isWithinLBreakpoint }}
updateButtonProps={{ iconOnly: isWithinLBreakpoint, fill: false }}
width={width}
/>
</EuiFlexItem>
{showRefresh === true || !isTimeRangeSelectorEnabled ? (
<EuiFlexItem grow={false}>
<EuiButton
fill
fill={false}
color="primary"
iconType={'refresh'}
onClick={() => updateLastRefresh()}
@ -314,6 +314,16 @@ export const DatePickerWrapper: FC<DatePickerWrapperProps> = (props) => {
</EuiButton>
</EuiFlexItem>
) : null}
</>
);
const wrapped = flexGroup ? (
<EuiFlexGroup gutterSize="s" alignItems="center">
{flexItems}
</EuiFlexGroup>
) : null;
) : (
flexItems
);
return isAutoRefreshSelectorEnabled || isTimeRangeSelectorEnabled ? wrapped : null;
};

View file

@ -191,7 +191,7 @@ export const FullTimeRangeSelector: FC<FullTimeRangeSelectorProps> = (props) =>
);
return (
<EuiFlexGroup responsive={false} gutterSize="xs" alignItems="center">
<EuiFlexGroup responsive={false} gutterSize="xs">
<EuiToolTip content={buttonTooltip}>
<EuiButton
isDisabled={disabled}

View file

@ -11,10 +11,10 @@ import {
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiPageBody,
EuiPageContentBody_Deprecated as EuiPageContentBody,
EuiPageSection,
EuiPanel,
EuiSpacer,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@ -170,8 +170,8 @@ export const ExplainLogRateSpikesPage: FC = () => {
return (
<EuiPageBody data-test-subj="aiopsExplainLogRateSpikesPage" paddingSize="none" panelled={false}>
<PageHeader />
<EuiHorizontalRule />
<EuiPageContentBody>
<EuiSpacer size="m" />
<EuiPageSection paddingSize="none">
<EuiFlexGroup gutterSize="m" direction="column">
<EuiFlexItem>
<SearchPanel
@ -236,7 +236,7 @@ export const ExplainLogRateSpikesPage: FC = () => {
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContentBody>
</EuiPageSection>
</EuiPageBody>
);
};

View file

@ -5,17 +5,10 @@
* 2.0.
*/
import { css } from '@emotion/react';
import React, { FC, useCallback, useMemo } from 'react';
import {
useIsWithinMaxBreakpoint,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiTitle,
EuiPageContentHeader_Deprecated as EuiPageContentHeader,
EuiPageContentHeaderSection_Deprecated as EuiPageContentHeaderSection,
} from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiPageHeader } from '@elastic/eui';
import { useUrlState } from '@kbn/ml-url-state';
import { useStorage } from '@kbn/ml-local-storage';
@ -27,7 +20,6 @@ import {
FROZEN_TIER_PREFERENCE,
} from '@kbn/ml-date-picker';
import { useCss } from '../../hooks/use_css';
import { useDataSource } from '../../hooks/use_data_source';
import {
AIOPS_FROZEN_TIER_PREFERENCE,
@ -35,9 +27,11 @@ import {
type AiOpsStorageMapped,
} from '../../types/storage';
export const PageHeader: FC = () => {
const { aiopsPageHeader, dataViewTitleHeader } = useCss();
const dataViewTitleHeader = css({
minWidth: '300px',
});
export const PageHeader: FC = () => {
const [, setGlobalState] = useUrlState('_g');
const { dataView } = useDataSource();
@ -67,49 +61,32 @@ export const PageHeader: FC = () => {
[dataView.timeFieldName]
);
const isWithinLBreakpoint = useIsWithinMaxBreakpoint('l');
return (
<EuiFlexGroup gutterSize="none">
<EuiFlexItem>
<EuiPageContentHeader css={aiopsPageHeader}>
<EuiPageContentHeaderSection>
<div css={dataViewTitleHeader}>
<EuiTitle size="s">
<h2>{dataView.getName()}</h2>
</EuiTitle>
</div>
</EuiPageContentHeaderSection>
{isWithinLBreakpoint ? <EuiSpacer size="m" /> : null}
<EuiFlexGroup
alignItems="center"
justifyContent="flexEnd"
gutterSize="s"
data-test-subj="aiopsTimeRangeSelectorSection"
>
{hasValidTimeField ? (
<EuiFlexItem grow={false}>
<FullTimeRangeSelector
frozenDataPreference={frozenDataPreference}
setFrozenDataPreference={setFrozenDataPreference}
dataView={dataView}
query={undefined}
disabled={false}
timefilter={timefilter}
callback={updateTimeState}
/>
</EuiFlexItem>
) : null}
<EuiPageHeader
pageTitle={<div css={dataViewTitleHeader}>{dataView.getName()}</div>}
rightSideItems={[
<EuiFlexGroup gutterSize="s" data-test-subj="aiopsTimeRangeSelectorSection">
{hasValidTimeField ? (
<EuiFlexItem grow={false}>
<DatePickerWrapper
isAutoRefreshOnly={!hasValidTimeField}
showRefresh={!hasValidTimeField}
<FullTimeRangeSelector
frozenDataPreference={frozenDataPreference}
setFrozenDataPreference={setFrozenDataPreference}
dataView={dataView}
query={undefined}
disabled={false}
timefilter={timefilter}
callback={updateTimeState}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContentHeader>
</EuiFlexItem>
</EuiFlexGroup>
) : null}
<DatePickerWrapper
isAutoRefreshOnly={!hasValidTimeField}
showRefresh={!hasValidTimeField}
width="full"
flexGroup={false}
/>
</EuiFlexGroup>,
]}
/>
);
};

View file

@ -93,13 +93,8 @@ export const SearchPanel: FC<Props> = ({
};
return (
<EuiFlexGroup
gutterSize="s"
data-test-subj="aiopsSearchPanel"
className={'aiopsSearchPanel__container'}
responsive={false}
>
<EuiFlexItem grow={9} className={'aiopsSearchBar'}>
<EuiFlexGroup gutterSize="s" data-test-subj="aiopsSearchPanel" responsive={false}>
<EuiFlexItem grow={9}>
<SearchBar
dataTestSubj="aiopsQueryInput"
appName={'aiops'}
@ -116,7 +111,7 @@ export const SearchPanel: FC<Props> = ({
})}
displayStyle={'inPage'}
isClearable={true}
customSubmitButton={<div />}
showSubmitButton={false}
onFiltersUpdated={(filters: Filter[]) => searchHandler({ filters })}
/>
</EuiFlexItem>

View file

@ -1,31 +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 { css } from '@emotion/react';
import { useEuiBreakpoint } from '@elastic/eui';
import { euiThemeVars } from '@kbn/ui-theme';
// Define fully static CSS outside hook.
const dataViewTitleHeader = css({
minWidth: '300px',
padding: `${euiThemeVars.euiSizeS} 0`,
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
});
export const useCss = () => {
// Define CSS referencing inline dependencies within hook.
const aiopsPageHeader = css({
[useEuiBreakpoint(['xs', 's', 'm', 'l'])]: {
flexDirection: 'column',
alignItems: 'flex-start',
},
});
return { dataViewTitleHeader, aiopsPageHeader };
};

View file

@ -528,6 +528,7 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
<DatePickerWrapper
isAutoRefreshOnly={!hasValidTimeField}
showRefresh={!hasValidTimeField}
width="full"
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -6,16 +6,18 @@
*/
import React, { createContext, FC, useEffect, useMemo, useState } from 'react';
import { Subscription } from 'rxjs';
import { EuiPageContentBody_Deprecated as EuiPageContentBody } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Redirect, Route, Switch } from 'react-router-dom';
import type { AppMountParameters } from '@kbn/core/public';
import { KibanaPageTemplate, RedirectAppLinks } from '@kbn/kibana-react-plugin/public';
import { createHtmlPortalNode, HtmlPortalNode } from 'react-reverse-portal';
import { Redirect, Route, Switch } from 'react-router-dom';
import { Subscription } from 'rxjs';
import { EuiPageSection } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { type AppMountParameters } from '@kbn/core/public';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
import { DatePickerWrapper } from '@kbn/ml-date-picker';
import { MlPageHeaderRenderer } from '../page_header/page_header';
import { useSideNavItems } from './side_nav';
import * as routes from '../../routing/routes';
import { MlPageWrapper } from '../../routing/ml_page_wrapper';
import { useMlKibana, useNavigateToPath } from '../../contexts/kibana';
@ -23,6 +25,12 @@ import { MlRoute, PageDependencies } from '../../routing/router';
import { useActiveRoute } from '../../routing/use_active_route';
import { useDocTitle } from '../../routing/use_doc_title';
import { MlPageHeaderRenderer } from '../page_header/page_header';
import { useSideNavItems } from './side_nav';
const ML_APP_SELECTOR = '[data-test-subj="mlApp"]';
export const MlPageControlsContext = createContext<{
headerPortal: HtmlPortalNode;
setHeaderActionMenu?: AppMountParameters['setHeaderActionMenu'];
@ -79,11 +87,30 @@ export const MlPage: FC<{ pageDeps: PageDependencies }> = React.memo(({ pageDeps
const activeRoute = useActiveRoute(routeList);
const rightSideItems = useMemo(() => {
return [...(activeRoute.enableDatePicker ? [<DatePickerWrapper isLoading={isLoading} />] : [])];
return [
...(activeRoute.enableDatePicker
? [<DatePickerWrapper isLoading={isLoading} width="full" />]
: []),
];
}, [activeRoute.enableDatePicker, isLoading]);
useDocTitle(activeRoute);
// The deprecated `KibanaPageTemplate` from`'@kbn/kibana-react-plugin/public'`
// had a `pageBodyProps` prop where we could pass in the `data-test-subj` for
// the `main` element. This is no longer available in the update template
// imported from `'@kbn/shared-ux-page-kibana-template'`. The following is a
// workaround to add the `data-test-subj` on the `main` element again.
useEffect(() => {
const mlApp = document.querySelector(ML_APP_SELECTOR) as HTMLElement;
if (mlApp && typeof activeRoute?.['data-test-subj'] === 'string') {
const mlAppMain = mlApp.querySelector('main') as HTMLElement;
if (mlAppMain) {
mlAppMain.setAttribute('data-test-subj', activeRoute?.['data-test-subj']);
}
}
}, [activeRoute]);
return (
<MlPageControlsContext.Provider
value={{
@ -97,11 +124,6 @@ export const MlPage: FC<{ pageDeps: PageDependencies }> = React.memo(({ pageDeps
className={'ml-app'}
data-test-subj={'mlApp'}
restrictWidth={false}
// EUI TODO
// The different template options need to be manually recreated by the individual pages.
// These classes help enforce the layouts.
pageContentProps={{ className: 'kbnAppWrapper' }}
pageContentBodyProps={{ className: 'kbnAppWrapper' }}
solutionNav={{
name: i18n.translate('xpack.ml.plugin.title', {
defaultMessage: 'Machine Learning',
@ -114,9 +136,6 @@ export const MlPage: FC<{ pageDeps: PageDependencies }> = React.memo(({ pageDeps
rightSideItems,
restrictWidth: false,
}}
pageBodyProps={{
'data-test-subj': activeRoute?.['data-test-subj'],
}}
>
<CommonPageWrapper
headerPortal={headerPortalNode}
@ -144,8 +163,8 @@ const CommonPageWrapper: FC<CommonPageWrapperProps> = React.memo(({ pageDeps, ro
return (
/** RedirectAppLinks intercepts all <a> tags to use navigateToUrl
* avoiding full page reload **/
<RedirectAppLinks application={application}>
<EuiPageContentBody restrictWidth={false}>
<RedirectAppLinks coreStart={{ application }}>
<EuiPageSection restrictWidth={false}>
<Switch>
{routeList.map((route) => {
return (
@ -166,7 +185,7 @@ const CommonPageWrapper: FC<CommonPageWrapperProps> = React.memo(({ pageDeps, ro
})}
<Redirect to="/overview" />
</Switch>
</EuiPageContentBody>
</EuiPageSection>
</RedirectAppLinks>
);
});

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export { PageTitle } from './page_title';

View file

@ -0,0 +1,19 @@
/*
* 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 { css } from '@emotion/react';
import React, { FC } from 'react';
const cssPageTitle = css({
minWidth: '300px',
});
interface PageTitleProps {
title: string;
}
export const PageTitle: FC<PageTitleProps> = ({ title }) => <div css={cssPageTitle}>{title}</div>;

View file

@ -7,7 +7,7 @@
import React, { FC, useState } from 'react';
import { EuiPanel, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { mlTimefilterRefresh$, useTimefilter } from '@kbn/ml-date-picker';
import { checkPermission } from '../capabilities/check_capabilities';
import { mlNodesAvailable } from '../ml_nodes_check';
@ -21,6 +21,7 @@ import { HelpMenu } from '../components/help_menu';
import { useMlKibana } from '../contexts/kibana';
import { NodesList } from '../trained_models/nodes_overview';
import { MlPageHeader } from '../components/page_header';
import { PageTitle } from '../components/page_title';
export const OverviewPage: FC = () => {
const canViewMlNodes = checkPermission('canViewMlNodes');
@ -39,7 +40,11 @@ export const OverviewPage: FC = () => {
return (
<div>
<MlPageHeader>
<FormattedMessage id="xpack.ml.overview.overviewLabel" defaultMessage="Overview" />
<PageTitle
title={i18n.translate('xpack.ml.overview.overviewLabel', {
defaultMessage: 'Overview',
})}
/>
</MlPageHeader>
<NodeAvailableWarning />
<JobsAwaitingNodeWarning jobCount={adLazyJobCount + dfaLazyJobCount} />

View file

@ -11,7 +11,6 @@ import useObservable from 'react-use/lib/useObservable';
import { i18n } from '@kbn/i18n';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiThemeProvider as StyledComponentsThemeProvider } from '@kbn/kibana-react-plugin/common';
import { useUrlState } from '@kbn/ml-url-state';
import { useTimefilter } from '@kbn/ml-date-picker';
@ -38,6 +37,7 @@ import { MlAnnotationUpdatesContext } from '../../contexts/ml/ml_annotation_upda
import { AnnotationUpdatesService } from '../../services/annotations_service';
import { useTimeBuckets } from '../../components/custom_hooks/use_time_buckets';
import { MlPageHeader } from '../../components/page_header';
import { PageTitle } from '../../components/page_title';
import { AnomalyResultsViewSelector } from '../../components/anomaly_results_view_selector';
import { AnomalyDetectionEmptyState } from '../../jobs/jobs_list/components/anomaly_detection_empty_state';
import {
@ -263,7 +263,11 @@ const ExplorerUrlStateManager: FC<ExplorerUrlStateManagerProps> = ({ jobsWithTim
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<FormattedMessage id="xpack.ml.explorer.pageTitle" defaultMessage="Anomaly Explorer" />
<PageTitle
title={i18n.translate('xpack.ml.explorer.pageTitle', {
defaultMessage: 'Anomaly Explorer',
})}
/>
</EuiFlexItem>
</EuiFlexGroup>
</MlPageHeader>

View file

@ -7,7 +7,7 @@
import React, { FC } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
@ -17,6 +17,7 @@ import { JobSelector } from '../components/job_selector';
import { HelpMenu } from '../components/help_menu';
import { useMlKibana } from '../contexts/kibana';
import { MlPageHeader } from '../components/page_header';
import { PageTitle } from '../components/page_title';
interface TimeSeriesExplorerPageProps {
dateFormatTz?: string;
@ -47,9 +48,10 @@ export const TimeSeriesExplorerPage: FC<TimeSeriesExplorerPageProps> = ({
<AnomalyResultsViewSelector viewId="timeseriesexplorer" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<FormattedMessage
id="xpack.ml.timeSeriesExplorer.pageTitle"
defaultMessage="Single Metric Viewer"
<PageTitle
title={i18n.translate('xpack.ml.timeSeriesExplorer.pageTitle', {
defaultMessage: 'Single Metric Viewer',
})}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -75,5 +75,7 @@
"@kbn/ml-date-picker",
"@kbn/ml-is-defined",
"@kbn/ml-query-utils",
"@kbn/shared-ux-page-kibana-template",
"@kbn/shared-ux-link-redirect-app",
],
}