mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
* create NavigationMenu and TopNav components * create timefilter wrapper class * update timefilter type file * use new navMenu * use legacy timefilter for initial conversion * remove comments * Move navMenu tabs into separate component * ensure dataFrame tab selected style works * update test * remove top padding when topNav not visible * add refresh button functionality
This commit is contained in:
parent
215931326b
commit
9ba01c9ab2
16 changed files with 387 additions and 8 deletions
|
@ -47,6 +47,10 @@ describe('DashboardState', function() {
|
|||
enableAutoRefreshSelector: jest.fn(),
|
||||
off: jest.fn(),
|
||||
on: jest.fn(),
|
||||
getActiveBounds: () => {},
|
||||
enableTimeRangeSelector: () => {},
|
||||
isAutoRefreshSelectorEnabled: true,
|
||||
isTimeRangeSelectorEnabled: true,
|
||||
};
|
||||
const mockIndexPattern: IndexPattern = { id: 'index1', fields: [], title: 'hi' };
|
||||
|
||||
|
|
|
@ -33,11 +33,15 @@ export interface Timefilter {
|
|||
setTime: (timeRange: TimeRange) => void;
|
||||
setRefreshInterval: (refreshInterval: RefreshInterval) => void;
|
||||
getRefreshInterval: () => RefreshInterval;
|
||||
getActiveBounds: () => void;
|
||||
disableAutoRefreshSelector: () => void;
|
||||
disableTimeRangeSelector: () => void;
|
||||
enableAutoRefreshSelector: () => void;
|
||||
enableTimeRangeSelector: () => void;
|
||||
off: (event: string, reload: () => void) => void;
|
||||
on: (event: string, reload: () => void) => void;
|
||||
isAutoRefreshSelectorEnabled: boolean;
|
||||
isTimeRangeSelectorEnabled: boolean;
|
||||
}
|
||||
|
||||
export const timefilter: Timefilter;
|
||||
|
|
|
@ -29,7 +29,7 @@ import 'plugins/ml/components/form_label';
|
|||
import 'plugins/ml/components/json_tooltip';
|
||||
import 'plugins/ml/components/tooltip';
|
||||
import 'plugins/ml/components/confirm_modal';
|
||||
import 'plugins/ml/components/nav_menu';
|
||||
import 'plugins/ml/components/navigation_menu';
|
||||
import 'plugins/ml/components/loading_indicator';
|
||||
import 'plugins/ml/settings';
|
||||
import 'plugins/ml/file_datavisualizer';
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
@import 'navigation_menu'
|
|
@ -0,0 +1,7 @@
|
|||
.mlNavigationMenu__tab {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.mlNavigationMenu__topNav {
|
||||
padding-top: $euiSizeS;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 './navigation_menu_react_wrapper_directive';
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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, { Fragment, FC } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { TopNav } from './top_nav';
|
||||
import { Tabs } from './tabs';
|
||||
|
||||
interface Props {
|
||||
dateFormat: string;
|
||||
disableLinks: boolean;
|
||||
forceRefresh: () => void;
|
||||
showTabs: boolean;
|
||||
tabId: string;
|
||||
timeHistory: any;
|
||||
timefilter: any;
|
||||
}
|
||||
|
||||
export const NavigationMenu: FC<Props> = ({
|
||||
dateFormat,
|
||||
disableLinks,
|
||||
forceRefresh,
|
||||
showTabs,
|
||||
tabId,
|
||||
timeHistory,
|
||||
timefilter,
|
||||
}) => (
|
||||
<Fragment>
|
||||
<EuiFlexGroup justifyContent="flexEnd" gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<TopNav
|
||||
dateFormat={dateFormat}
|
||||
timeHistory={timeHistory}
|
||||
timefilter={timefilter}
|
||||
forceRefresh={forceRefresh}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{showTabs && <Tabs tabId={tabId} disableLinks={disableLinks} />}
|
||||
</Fragment>
|
||||
);
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { NavigationMenu } from './navigation_menu';
|
||||
import { isFullLicense } from '../../license/check_license';
|
||||
import { timeHistory } from 'ui/timefilter/time_history';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { timefilter } from 'ui/timefilter';
|
||||
import { Subject } from 'rxjs';
|
||||
const module = uiModules.get('apps/ml');
|
||||
|
||||
import 'ui/directives/kbn_href';
|
||||
|
||||
|
||||
module.directive('mlNavMenu', function (config, mlTimefilterRefreshService) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
transclude: true,
|
||||
link: function (scope, element, attrs) {
|
||||
const { name } = attrs;
|
||||
let showTabs = false;
|
||||
|
||||
if (name === 'jobs' ||
|
||||
name === 'settings' ||
|
||||
name === 'data_frames' ||
|
||||
name === 'datavisualizer' ||
|
||||
name === 'filedatavisualizer' ||
|
||||
name === 'timeseriesexplorer' ||
|
||||
name === 'access-denied' ||
|
||||
name === 'explorer') {
|
||||
showTabs = true;
|
||||
}
|
||||
|
||||
const props = {
|
||||
dateFormat: config.get('dateFormat'),
|
||||
disableLinks: (isFullLicense() === false),
|
||||
showTabs,
|
||||
tabId: name,
|
||||
timeHistory,
|
||||
timefilter,
|
||||
forceRefresh: () => mlTimefilterRefreshService.next()
|
||||
};
|
||||
|
||||
ReactDOM.render(React.createElement(NavigationMenu, props),
|
||||
element[0]
|
||||
);
|
||||
|
||||
element.on('$destroy', () => {
|
||||
ReactDOM.unmountComponentAtNode(element[0]);
|
||||
scope.$destroy();
|
||||
});
|
||||
}
|
||||
};
|
||||
})
|
||||
.service('mlTimefilterRefreshService', function () {
|
||||
return new Subject();
|
||||
});
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* 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 } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import chrome from 'ui/chrome';
|
||||
|
||||
interface Tab {
|
||||
id: string;
|
||||
name: any;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
interface TestSubjMap {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
disableLinks: boolean;
|
||||
tabId: string;
|
||||
}
|
||||
|
||||
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: 'Data Frames',
|
||||
}),
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const TAB_TEST_SUBJ_MAP: TestSubjMap = {
|
||||
jobs: 'mlTabJobManagement',
|
||||
explorer: 'mlTabAnomalyExplorer',
|
||||
timeseriesexplorer: 'mlTabSingleMetricViewer',
|
||||
data_frames: 'mlTabDataFrames',
|
||||
datavisualizer: 'mlTabDataVisualizer',
|
||||
settings: 'mlTabSettings',
|
||||
};
|
||||
|
||||
function moveToSelectedTab(selectedTabId: string) {
|
||||
window.location.href = `${chrome.getBasePath()}/app/ml#/${selectedTabId}`;
|
||||
}
|
||||
|
||||
export const Tabs: FC<Props> = ({ tabId, disableLinks }) => {
|
||||
const [selectedTabId, setSelectedTabId] = useState(tabId);
|
||||
function onSelectedTabChanged(id: string) {
|
||||
moveToSelectedTab(id);
|
||||
setSelectedTabId(id);
|
||||
}
|
||||
|
||||
const tabs = getTabs(disableLinks);
|
||||
|
||||
return (
|
||||
<EuiTabs>
|
||||
{tabs.map((tab: Tab) => (
|
||||
<EuiTab
|
||||
className="mlNavigationMenu__tab"
|
||||
onClick={() => onSelectedTabChanged(tab.id)}
|
||||
isSelected={tab.id === selectedTabId}
|
||||
disabled={tab.disabled}
|
||||
key={`${tab.id}-key`}
|
||||
data-test-subj={TAB_TEST_SUBJ_MAP[tab.id]}
|
||||
>
|
||||
{tab.name}
|
||||
</EuiTab>
|
||||
))}
|
||||
</EuiTabs>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { TopNav } from './top_nav';
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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, Fragment, useState, useEffect } from 'react';
|
||||
import { EuiSuperDatePicker } from '@elastic/eui';
|
||||
import { TimeHistory, TimeRange } from 'src/legacy/ui/public/timefilter/time_history';
|
||||
import { Timefilter } from 'ui/timefilter';
|
||||
|
||||
interface Props {
|
||||
dateFormat: string;
|
||||
forceRefresh: () => void;
|
||||
timeHistory: TimeHistory;
|
||||
timefilter: Timefilter;
|
||||
}
|
||||
|
||||
function getRecentlyUsedRanges(timeHistory: TimeHistory): Array<{ start: string; end: string }> {
|
||||
return timeHistory.get().map(({ from, to }: TimeRange) => {
|
||||
return {
|
||||
start: from,
|
||||
end: to,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export const TopNav: FC<Props> = ({ dateFormat, forceRefresh, timeHistory, timefilter }) => {
|
||||
const [refreshInterval, setRefreshInterval] = useState(timefilter.getRefreshInterval());
|
||||
const [time, setTime] = useState(timefilter.getTime());
|
||||
const [recentlyUsedRanges, setRecentlyUsedRanges] = useState(getRecentlyUsedRanges(timeHistory));
|
||||
const [isAutoRefreshSelectorEnabled, setIsAutoRefreshSelectorEnabled] = useState(
|
||||
timefilter.isAutoRefreshSelectorEnabled
|
||||
);
|
||||
const [isTimeRangeSelectorEnabled, setIsTimeRangeSelectorEnabled] = useState(
|
||||
timefilter.isTimeRangeSelectorEnabled
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
timefilter.on('refreshIntervalUpdate', timefilterUpdateListener);
|
||||
timefilter.on('timeUpdate', timefilterUpdateListener);
|
||||
timefilter.on('enabledUpdated', timefilterUpdateListener);
|
||||
|
||||
return function cleanup() {
|
||||
timefilter.off('refreshIntervalUpdate', timefilterUpdateListener);
|
||||
timefilter.off('timeUpdate', timefilterUpdateListener);
|
||||
timefilter.off('enabledUpdated', timefilterUpdateListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Force re-render with up-to-date values when isTimeRangeSelectorEnabled/isAutoRefreshSelectorEnabled are changed.
|
||||
timefilterUpdateListener();
|
||||
}, [isTimeRangeSelectorEnabled, isAutoRefreshSelectorEnabled]);
|
||||
|
||||
function timefilterUpdateListener() {
|
||||
setTime(timefilter.getTime());
|
||||
setRefreshInterval(timefilter.getRefreshInterval());
|
||||
setIsAutoRefreshSelectorEnabled(timefilter.isAutoRefreshSelectorEnabled);
|
||||
setIsTimeRangeSelectorEnabled(timefilter.isTimeRangeSelectorEnabled);
|
||||
}
|
||||
|
||||
function updateFilter({ start, end }: { start: string; end: string }) {
|
||||
const newTime = { from: start, to: end };
|
||||
// Update timefilter for controllers listening for changes
|
||||
timefilter.setTime(newTime);
|
||||
setTime(newTime);
|
||||
setRecentlyUsedRanges(getRecentlyUsedRanges(timeHistory));
|
||||
}
|
||||
|
||||
function updateInterval({
|
||||
isPaused,
|
||||
refreshInterval: interval,
|
||||
}: {
|
||||
isPaused: boolean;
|
||||
refreshInterval: number;
|
||||
}) {
|
||||
const newInterval = {
|
||||
pause: isPaused,
|
||||
value: interval,
|
||||
};
|
||||
// Update timefilter for controllers listening for changes
|
||||
timefilter.setRefreshInterval(newInterval);
|
||||
// Update state
|
||||
setRefreshInterval(newInterval);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{(isAutoRefreshSelectorEnabled || isTimeRangeSelectorEnabled) && (
|
||||
<div className="mlNavigationMenu__topNav">
|
||||
<EuiSuperDatePicker
|
||||
start={time.from}
|
||||
end={time.to}
|
||||
isPaused={refreshInterval.pause}
|
||||
isAutoRefreshOnly={!isTimeRangeSelectorEnabled}
|
||||
refreshInterval={refreshInterval.value}
|
||||
onTimeChange={updateFilter}
|
||||
onRefresh={forceRefresh}
|
||||
onRefreshChange={updateInterval}
|
||||
recentlyUsedRanges={recentlyUsedRanges}
|
||||
dateFormat={dateFormat}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -15,7 +15,7 @@ import { loadIndexPatterns } from '../../../util/index_utils';
|
|||
// @ts-ignore
|
||||
import { getDataFrameBreadcrumbs } from '../../breadcrumbs';
|
||||
|
||||
const template = `<ml-nav-menu name="data_frame" /><ml-data-frame-page />`;
|
||||
const template = `<ml-nav-menu name="data_frames" /><ml-data-frame-page />`;
|
||||
|
||||
uiRoutes.when('/data_frames/?', {
|
||||
template,
|
||||
|
|
|
@ -52,10 +52,11 @@ import { uiModules } from 'ui/modules';
|
|||
const module = uiModules.get('apps/ml');
|
||||
|
||||
module
|
||||
.controller('MlDataVisualizerViewFields', function ($scope, $timeout, $window, Private, AppState, config) {
|
||||
.controller('MlDataVisualizerViewFields', function ($injector, $scope, $timeout, $window, Private, AppState, config) {
|
||||
|
||||
timefilter.enableTimeRangeSelector();
|
||||
timefilter.enableAutoRefreshSelector();
|
||||
const mlTimefilterRefreshService = $injector.get('mlTimefilterRefreshService');
|
||||
|
||||
const createSearchItems = Private(SearchItemsProvider);
|
||||
const {
|
||||
|
@ -144,12 +145,21 @@ module
|
|||
.value();
|
||||
$scope.indexedFieldTypes = indexedFieldTypes.sort();
|
||||
|
||||
|
||||
// Refresh the data when the time range is altered.
|
||||
$scope.$listenAndDigestAsync(timefilter, 'fetch', function () {
|
||||
function refresh() {
|
||||
$scope.earliest = timefilter.getActiveBounds().min.valueOf();
|
||||
$scope.latest = timefilter.getActiveBounds().max.valueOf();
|
||||
loadOverallStats();
|
||||
}
|
||||
|
||||
// Refresh the data when the time range is altered.
|
||||
$scope.$listenAndDigestAsync(timefilter, 'fetch', function () {
|
||||
refresh();
|
||||
});
|
||||
|
||||
const timefilterRefreshServiceSub = mlTimefilterRefreshService.subscribe(refresh);
|
||||
|
||||
$scope.$on('$destroy', () => {
|
||||
timefilterRefreshServiceSub.unsubscribe();
|
||||
});
|
||||
|
||||
$scope.submitSearchQuery = function () {
|
||||
|
|
|
@ -71,6 +71,7 @@ module.controller('MlExplorerController', function (
|
|||
$injector.get('mlSelectSeverityService');
|
||||
|
||||
const mlJobSelectService = $injector.get('mlJobSelectService');
|
||||
const mlTimefilterRefreshService = $injector.get('mlTimefilterRefreshService');
|
||||
|
||||
// $scope should only contain what's actually still necessary for the angular part.
|
||||
// For the moment that's the job selector and the (hidden) filter bar.
|
||||
|
@ -203,6 +204,12 @@ module.controller('MlExplorerController', function (
|
|||
}
|
||||
});
|
||||
|
||||
const timefilterRefreshServiceSub = mlTimefilterRefreshService.subscribe(() => {
|
||||
if ($scope.jobSelectionUpdateInProgress === false) {
|
||||
explorer$.next({ action: EXPLORER_ACTION.RELOAD });
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh all the data when the time range is altered.
|
||||
$scope.$listenAndDigestAsync(timefilter, 'fetch', () => {
|
||||
if ($scope.jobSelectionUpdateInProgress === false) {
|
||||
|
@ -287,6 +294,7 @@ module.controller('MlExplorerController', function (
|
|||
$scope.$on('$destroy', () => {
|
||||
explorerSubscriber.unsubscribe();
|
||||
jobSelectServiceSub.unsubscribe();
|
||||
timefilterRefreshServiceSub.unsubscribe();
|
||||
refreshWatcher.cancel();
|
||||
$(window).off('resize', jqueryRedrawOnResize);
|
||||
// Cancel listening for updates to the global nav state.
|
||||
|
|
|
@ -40,11 +40,11 @@
|
|||
@import 'components/form_label/index';
|
||||
@import 'components/influencers_list/index';
|
||||
@import 'components/items_grid/index';
|
||||
@import 'components/job_selector/index'; // TODO: remove above two once react conversion of job selector is done
|
||||
@import 'components/job_selector/index';
|
||||
@import 'components/json_tooltip/index'; // SASSTODO: This file overwrites EUI directly
|
||||
@import 'components/loading_indicator/index'; // SASSTODO: This component should be replaced with EuiLoadingSpinner
|
||||
@import 'components/messagebar/index';
|
||||
@import 'components/nav_menu/index';
|
||||
@import 'components/navigation_menu/index';
|
||||
@import 'components/rule_editor/index'; // SASSTODO: This file overwrites EUI directly
|
||||
|
||||
// Hacks are last so they can overwrite anything above if needed
|
||||
|
|
|
@ -93,6 +93,7 @@ module.controller('MlTimeSeriesExplorerController', function (
|
|||
$injector.get('mlSelectIntervalService');
|
||||
$injector.get('mlSelectSeverityService');
|
||||
const mlJobSelectService = $injector.get('mlJobSelectService');
|
||||
const mlTimefilterRefreshService = $injector.get('mlTimefilterRefreshService');
|
||||
|
||||
$scope.timeFieldName = 'timestamp';
|
||||
timefilter.enableTimeRangeSelector();
|
||||
|
@ -711,12 +712,15 @@ module.controller('MlTimeSeriesExplorerController', function (
|
|||
}
|
||||
});
|
||||
|
||||
const timefilterRefreshServiceSub = mlTimefilterRefreshService.subscribe($scope.refresh);
|
||||
|
||||
$scope.$on('$destroy', () => {
|
||||
refreshWatcher.cancel();
|
||||
intervalSub.unsubscribe();
|
||||
severitySub.unsubscribe();
|
||||
annotationsRefreshSub.unsubscribe();
|
||||
jobSelectServiceSub.unsubscribe();
|
||||
timefilterRefreshServiceSub.unsubscribe();
|
||||
});
|
||||
|
||||
$scope.$on('contextChartSelected', function (event, selection) { // eslint-disable-line no-unused-vars
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue