[ML] Reorganize ML navigation with top and sub level tabs (#45220)

* Add main nav tabs with sub tabs for new nav

* move transforms to top level main nav

* Make top nav normal font weight

* Update breadcrumbs to take top nav into account

* proper spacing when settings selected

* fix localization error

* Fix functional tests. Update breadcrumbs

* revert analytics breadcrumb update. save for follow up

* ensure main/sub tabs align left

* update dataVisualizer breadcrumbs

* update typescript for tabs
This commit is contained in:
Melissa Alvarez 2019-09-12 09:09:36 -04:00 committed by GitHub
parent a45b0a7bbf
commit 9e3a3dc7c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 298 additions and 106 deletions

View file

@ -12,3 +12,24 @@ export const ML_BREADCRUMB = Object.freeze({
}),
href: '#/'
});
export const SETTINGS = Object.freeze({
text: i18n.translate('xpack.ml.settingsBreadcrumbLabel', {
defaultMessage: 'Settings'
}),
href: '#/settings?'
});
export const ANOMALY_DETECTION_BREADCRUMB = Object.freeze({
text: i18n.translate('xpack.ml.anomalyDetectionBreadcrumbLabel', {
defaultMessage: 'Anomaly Detection'
}),
href: '#/jobs?'
});
export const DATA_VISUALIZER_BREADCRUMB = Object.freeze({
text: i18n.translate('xpack.ml.datavisualizerBreadcrumbLabel', {
defaultMessage: 'Data Visualizer'
}),
href: '#/datavisualizer?'
});

View file

@ -1,7 +1,24 @@
.mlNavigationMenu__tab {
padding-bottom: 0;
padding-left: 0px;
padding-right: 0px;
margin-left: $euiSizeM;
}
.mlNavigationMenu__mainTab {
margin-left: $euiSizeM;
padding-bottom: 0;
font-weight: normal;
}
.mlNavigationMenu__topNav {
padding-top: $euiSizeS;
}
.mlNavHorizontalRule {
margin: $euiSizeM 0 0 0;
}
.mlSubTabs {
margin-top: $euiSizeS;
}

View file

@ -0,0 +1,110 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, useState } from 'react';
import { EuiTabs, EuiTab, EuiLink } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import chrome from 'ui/chrome';
import { TabId } from './navigation_menu';
export interface Tab {
id: TabId;
name: any;
disabled: boolean;
}
interface Props {
disableLinks: boolean;
tabId: TabId;
}
function getTabs(disableLinks: boolean): Tab[] {
return [
// {
// id: 'overview',
// name: i18n.translate('xpack.ml.navMenu.overviewTabLinkText', {
// defaultMessage: 'Overview',
// }),
// disabled: disableLinks,
// },
{
id: 'anomaly_detection',
name: i18n.translate('xpack.ml.navMenu.anomalyDetectionTabLinkText', {
defaultMessage: 'Anomaly Detection',
}),
disabled: disableLinks,
},
{
id: 'data_frames',
name: i18n.translate('xpack.ml.navMenu.dataFrameTabLinkText', {
defaultMessage: 'Transforms',
}),
disabled: false,
},
{
id: 'data_frame_analytics',
name: i18n.translate('xpack.ml.navMenu.dataFrameAnalyticsTabLinkText', {
defaultMessage: 'Data Frame Analytics',
}),
disabled: disableLinks,
},
{
id: 'datavisualizer',
name: i18n.translate('xpack.ml.navMenu.dataVisualizerTabLinkText', {
defaultMessage: 'Data Visualizer',
}),
disabled: false,
},
];
}
interface TabData {
testSubject: string;
pathId?: string;
}
const TAB_DATA: Record<TabId, TabData> = {
// overview: { testSubject: 'mlTabOverview', pathId: 'overview' },
anomaly_detection: { testSubject: 'mlTabAnomalyDetection', pathId: 'jobs' },
data_frames: { testSubject: 'mlTabDataFrames' },
data_frame_analytics: { testSubject: 'mlTabDataFrameAnalytics' },
datavisualizer: { testSubject: 'mlTabDataVisualizer' },
};
export const MainTabs: FC<Props> = ({ tabId, disableLinks }) => {
const [selectedTabId, setSelectedTabId] = useState(tabId);
function onSelectedTabChanged(id: string) {
setSelectedTabId(id);
}
const tabs = getTabs(disableLinks);
return (
<EuiTabs display="condensed">
{tabs.map((tab: Tab) => {
const id = tab.id;
const testSubject = TAB_DATA[id].testSubject;
const defaultPathId = TAB_DATA[id].pathId || id;
return (
<EuiLink
data-test-subj={testSubject}
href={`${chrome.getBasePath()}/app/ml#/${defaultPathId}`}
key={`${id}-key`}
color="text"
>
<EuiTab
className={'mlNavigationMenu__mainTab'}
onClick={() => onSelectedTabChanged(id)}
isSelected={id === selectedTabId}
disabled={tab.disabled}
>
{tab.name}
</EuiTab>
</EuiLink>
);
})}
</EuiTabs>
);
};

View file

@ -5,42 +5,54 @@
*/
import React, { Fragment, FC } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
// @ts-ignore
import { isFullLicense } from '../../license/check_license';
import { TopNav } from './top_nav';
import { MainTabs } from './main_tabs';
import { Tabs } from './tabs';
const tabSupport = [
'jobs',
'settings',
'data_frames',
'data_frame_analytics',
'datavisualizer',
'filedatavisualizer',
'timeseriesexplorer',
'access-denied',
'explorer',
];
export type TabId = string;
type TabSupport = Record<TabId, string | null>;
const tabSupport: TabSupport = {
// overview: null,
jobs: 'anomaly_detection',
settings: 'anomaly_detection',
data_frames: null,
data_frame_analytics: null,
datavisualizer: null,
filedatavisualizer: null,
timeseriesexplorer: 'anomaly_detection',
'access-denied': null,
explorer: 'anomaly_detection',
};
interface Props {
tabId: string;
tabId: TabId;
}
export const NavigationMenu: FC<Props> = ({ tabId }) => {
const disableLinks = isFullLicense() === false;
const showTabs = tabSupport.includes(tabId);
const showTabs = typeof tabSupport[tabId] !== 'undefined';
const mainTabId = tabSupport[tabId] || tabId;
// show horizontal rule if there are no subtabs
const showHorizontalRule = tabSupport[tabId] === null;
return (
<Fragment>
<EuiFlexGroup justifyContent="flexEnd" gutterSize="xs">
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
{showTabs && <MainTabs tabId={mainTabId} disableLinks={disableLinks} />}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<TopNav />
</EuiFlexItem>
</EuiFlexGroup>
{showTabs && <Tabs tabId={tabId} disableLinks={disableLinks} />}
{showHorizontalRule && <EuiHorizontalRule className="mlNavHorizontalRule" />}
{showTabs && <Tabs tabId={tabId} mainTabId={mainTabId} disableLinks={disableLinks} />}
</Fragment>
);
};

View file

@ -8,94 +8,77 @@ import React, { FC, useState } from 'react';
import { EuiTabs, EuiTab, EuiLink } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import chrome from 'ui/chrome';
interface Tab {
id: string;
name: any;
disabled: boolean;
}
import { Tab } from './main_tabs';
import { TabId } from './navigation_menu';
interface Props {
disableLinks: boolean;
tabId: string;
mainTabId: TabId;
tabId: TabId;
}
function getTabs(disableLinks: boolean): Tab[] {
return [
{
id: 'jobs',
name: i18n.translate('xpack.ml.navMenu.jobManagementTabLinkText', {
defaultMessage: 'Job Management',
}),
disabled: disableLinks,
},
{
id: 'explorer',
name: i18n.translate('xpack.ml.navMenu.anomalyExplorerTabLinkText', {
defaultMessage: 'Anomaly Explorer',
}),
disabled: disableLinks,
},
{
id: 'timeseriesexplorer',
name: i18n.translate('xpack.ml.navMenu.singleMetricViewerTabLinkText', {
defaultMessage: 'Single Metric Viewer',
}),
disabled: disableLinks,
},
{
id: 'data_frames',
name: i18n.translate('xpack.ml.navMenu.dataFrameTabLinkText', {
defaultMessage: 'Transforms',
}),
disabled: false,
},
{
id: 'data_frame_analytics',
name: i18n.translate('xpack.ml.navMenu.dataFrameAnalyticsTabLinkText', {
defaultMessage: 'Analytics',
}),
disabled: disableLinks,
},
{
id: 'datavisualizer',
name: i18n.translate('xpack.ml.navMenu.dataVisualizerTabLinkText', {
defaultMessage: 'Data Visualizer',
}),
disabled: false,
},
{
id: 'settings',
name: i18n.translate('xpack.ml.navMenu.settingsTabLinkText', {
defaultMessage: 'Settings',
}),
disabled: disableLinks,
},
];
function getTabs(tabId: TabId, disableLinks: boolean): Tab[] {
const TAB_MAP: Record<TabId, Tab[]> = {
// overview: [],
datavisualizer: [],
data_frames: [],
data_frame_analytics: [],
anomaly_detection: [
{
id: 'jobs',
name: i18n.translate('xpack.ml.navMenu.jobManagementTabLinkText', {
defaultMessage: 'Job Management',
}),
disabled: disableLinks,
},
{
id: 'explorer',
name: i18n.translate('xpack.ml.navMenu.anomalyExplorerTabLinkText', {
defaultMessage: 'Anomaly Explorer',
}),
disabled: disableLinks,
},
{
id: 'timeseriesexplorer',
name: i18n.translate('xpack.ml.navMenu.singleMetricViewerTabLinkText', {
defaultMessage: 'Single Metric Viewer',
}),
disabled: disableLinks,
},
{
id: 'settings',
name: i18n.translate('xpack.ml.navMenu.settingsTabLinkText', {
defaultMessage: 'Settings',
}),
disabled: disableLinks,
},
],
};
return TAB_MAP[tabId];
}
enum TAB_TEST_SUBJECT {
// overview = 'mlOverview',
jobs = 'mlTabJobManagement',
explorer = 'mlTabAnomalyExplorer',
timeseriesexplorer = 'mlTabSingleMetricViewer',
data_frames = 'mlTabDataFrames', // eslint-disable-line
data_frame_analytics = 'mlTabDataFrameAnalytics', // eslint-disable-line
datavisualizer = 'mlTabDataVisualizer',
settings = 'mlTabSettings',
}
type TAB_TEST_SUBJECTS = keyof typeof TAB_TEST_SUBJECT;
export const Tabs: FC<Props> = ({ tabId, disableLinks }) => {
export const Tabs: FC<Props> = ({ tabId, mainTabId, disableLinks }) => {
const [selectedTabId, setSelectedTabId] = useState(tabId);
function onSelectedTabChanged(id: string) {
setSelectedTabId(id);
}
const tabs = getTabs(disableLinks);
const tabs = getTabs(mainTabId, disableLinks);
return (
<EuiTabs>
<EuiTabs size="s" className={tabId === 'settings' ? 'mlSubTabs' : ''}>
{tabs.map((tab: Tab) => {
const id = tab.id;
return (

View file

@ -14,7 +14,7 @@ export function getDataFrameAnalyticsBreadcrumbs() {
ML_BREADCRUMB,
{
text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameLabel', {
defaultMessage: 'Analytics',
defaultMessage: 'Data Frame Analytics',
}),
href: '',
},

View file

@ -5,10 +5,10 @@
*/
// @ts-ignore
import { ML_BREADCRUMB } from '../breadcrumbs';
import { ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB } from '../breadcrumbs';
export function getDataVisualizerBreadcrumbs() {
// Whilst top level nav menu with tabs remains,
// use root ML breadcrumb.
return [ML_BREADCRUMB];
return [ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB];
}

View file

@ -5,14 +5,22 @@
*/
import { ML_BREADCRUMB } from '../../breadcrumbs';
import { ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB } from '../../breadcrumbs';
import { i18n } from '@kbn/i18n';
export function getFileDataVisualizerBreadcrumbs() {
// Whilst top level nav menu with tabs remains,
// use root ML breadcrumb.
return [
ML_BREADCRUMB
ML_BREADCRUMB,
DATA_VISUALIZER_BREADCRUMB,
{
text: i18n.translate('xpack.ml.dataVisualizer.fileBasedLabel', {
defaultMessage: 'File'
}),
href: ''
}
];
}

View file

@ -4,11 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
// @ts-ignore
import { ML_BREADCRUMB } from '../../breadcrumbs';
import { i18n } from '@kbn/i18n';
import {
ML_BREADCRUMB,
DATA_VISUALIZER_BREADCRUMB,
// @ts-ignore
} from '../../breadcrumbs';
export function getDataVisualizerBreadcrumbs() {
// Whilst top level nav menu with tabs remains,
// use root ML breadcrumb.
return [ML_BREADCRUMB];
return [
ML_BREADCRUMB,
DATA_VISUALIZER_BREADCRUMB,
{
text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.indexLabel', {
defaultMessage: 'Index',
}),
href: '',
},
];
}

View file

@ -5,14 +5,22 @@
*/
import { ML_BREADCRUMB } from '../breadcrumbs';
import { ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB } from '../breadcrumbs';
import { i18n } from '@kbn/i18n';
export function getAnomalyExplorerBreadcrumbs() {
// Whilst top level nav menu with tabs remains,
// use root ML breadcrumb.
return [
ML_BREADCRUMB
ML_BREADCRUMB,
ANOMALY_DETECTION_BREADCRUMB,
{
text: i18n.translate('xpack.ml.anomalyDetection.anomalyExplorerLabel', {
defaultMessage: 'Anomaly Explorer'
}),
href: ''
}
];
}

View file

@ -5,7 +5,7 @@
*/
import { ML_BREADCRUMB } from '../breadcrumbs';
import { ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB } from '../breadcrumbs';
import { i18n } from '@kbn/i18n';
@ -13,13 +13,21 @@ export function getJobManagementBreadcrumbs() {
// Whilst top level nav menu with tabs remains,
// use root ML breadcrumb.
return [
ML_BREADCRUMB
ML_BREADCRUMB,
ANOMALY_DETECTION_BREADCRUMB,
{
text: i18n.translate('xpack.ml.anomalyDetection.jobManagementLabel', {
defaultMessage: 'Job Management'
}),
href: ''
}
];
}
export function getCreateJobBreadcrumbs() {
return [
ML_BREADCRUMB,
ANOMALY_DETECTION_BREADCRUMB,
{
text: i18n.translate('xpack.ml.jobsBreadcrumbs.createJobLabel', {
defaultMessage: 'Create job'
@ -90,6 +98,7 @@ export function getCreateRecognizerJobBreadcrumbs($routeParams) {
export function getDataVisualizerIndexOrSearchBreadcrumbs() {
return [
ML_BREADCRUMB,
DATA_VISUALIZER_BREADCRUMB,
{
text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel', {
defaultMessage: 'Select index or search'

View file

@ -5,7 +5,7 @@
*/
import { ML_BREADCRUMB } from '../breadcrumbs';
import { ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, SETTINGS } from '../breadcrumbs';
import { i18n } from '@kbn/i18n';
@ -13,7 +13,9 @@ export function getSettingsBreadcrumbs() {
// Whilst top level nav menu with tabs remains,
// use root ML breadcrumb.
return [
ML_BREADCRUMB
ML_BREADCRUMB,
ANOMALY_DETECTION_BREADCRUMB,
SETTINGS
];
}

View file

@ -5,14 +5,23 @@
*/
import { ML_BREADCRUMB } from '../breadcrumbs';
import { ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB } from '../breadcrumbs';
import { i18n } from '@kbn/i18n';
export function getSingleMetricViewerBreadcrumbs() {
// Whilst top level nav menu with tabs remains,
// use root ML breadcrumb.
return [
ML_BREADCRUMB
ML_BREADCRUMB,
ANOMALY_DETECTION_BREADCRUMB,
{
text: i18n.translate('xpack.ml.anomalyDetection.singleMetricViewerLabel', {
defaultMessage: 'Single Metric Viewer'
}),
href: ''
}
];
}

View file

@ -41,6 +41,12 @@ export default function({ getService }: FtrProviderContext) {
await ml.singleMetricViewer.assertSingleMetricViewerEmptyListMessageExsist();
});
it('loads the settings page', async () => {
await ml.navigation.navigateToSettings();
await ml.settings.assertSettingsCalendarLinkExists();
await ml.settings.assertSettingsFilterlistLinkExists();
});
it('loads the data frame page', async () => {
await ml.navigation.navigateToDataFrames();
await ml.dataFrames.assertDataFrameEmptyListMessageExists();
@ -51,11 +57,5 @@ export default function({ getService }: FtrProviderContext) {
await ml.dataVisualizer.assertDataVisualizerImportDataCardExists();
await ml.dataVisualizer.assertDataVisualizerIndexDataCardExists();
});
it('loads the settings page', async () => {
await ml.navigation.navigateToSettings();
await ml.settings.assertSettingsCalendarLinkExists();
await ml.settings.assertSettingsFilterlistLinkExists();
});
});
}

View file

@ -40,6 +40,10 @@ export function MachineLearningNavigationProvider({
await this.navigateToArea('mlTabSingleMetricViewer', 'mlPageSingleMetricViewer');
},
async navigateToSettings() {
await this.navigateToArea('mlTabSettings', 'mlPageSettings');
},
async navigateToDataFrames() {
await this.navigateToArea('mlTabDataFrames', 'mlPageDataFrame');
},
@ -47,9 +51,5 @@ export function MachineLearningNavigationProvider({
async navigateToDataVisualizer() {
await this.navigateToArea('mlTabDataVisualizer', 'mlPageDataVisualizerSelector');
},
async navigateToSettings() {
await this.navigateToArea('mlTabSettings', 'mlPageSettings');
},
};
}