[Unified Observability] Add feature flag for the new overview page (#119193)

* Add feature flag to display a blank overview page when enabled

* Add tests for overview page feature flag

* Fix types

* Fix more types

* Remove duplicated BucketSize type

* fix linter

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ester Martí Vilaseca 2021-11-29 17:06:25 +01:00 committed by GitHub
parent d9ee4d7ee3
commit 2c4196270a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 335 additions and 130 deletions

View file

@ -46,7 +46,13 @@ describe('renderApp', () => {
uiSettings: { get: () => false },
http: { basePath: { prepend: (path: string) => path } },
} as unknown as CoreStart;
const config = { unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } } };
const config = {
unsafe: {
alertingExperience: { enabled: true },
cases: { enabled: true },
overviewNext: { enabled: false },
},
};
const params = {
element: window.document.createElement('div'),
history: createMemoryHistory(),

View file

@ -42,7 +42,13 @@ describe('APMSection', () => {
http: { basePath: { prepend: jest.fn() } },
} as unknown as CoreStart,
appMountParameters: {} as AppMountParameters,
config: { unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } } },
config: {
unsafe: {
alertingExperience: { enabled: true },
cases: { enabled: true },
overviewNext: { enabled: false },
},
},
observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(),
plugins: {
data: {

View file

@ -42,7 +42,13 @@ describe('UXSection', () => {
http: { basePath: { prepend: jest.fn() } },
} as unknown as CoreStart,
appMountParameters: {} as AppMountParameters,
config: { unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } } },
config: {
unsafe: {
alertingExperience: { enabled: true },
cases: { enabled: true },
overviewNext: { enabled: false },
},
},
plugins: {
data: {
query: {

View file

@ -24,7 +24,13 @@ describe('useTimeRange', () => {
jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({
core: {} as CoreStart,
appMountParameters: {} as AppMountParameters,
config: { unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } } },
config: {
unsafe: {
alertingExperience: { enabled: true },
cases: { enabled: true },
overviewNext: { enabled: false },
},
},
plugins: {
data: {
query: {
@ -67,7 +73,13 @@ describe('useTimeRange', () => {
jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({
core: {} as CoreStart,
appMountParameters: {} as AppMountParameters,
config: { unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } } },
config: {
unsafe: {
alertingExperience: { enabled: true },
cases: { enabled: true },
overviewNext: { enabled: false },
},
},
plugins: {
data: {
query: {

View file

@ -26,7 +26,11 @@ export type {
export { enableInspectEsQueries } from '../common/ui_settings_keys';
export interface ConfigSchema {
unsafe: { alertingExperience: { enabled: boolean }; cases: { enabled: boolean } };
unsafe: {
alertingExperience: { enabled: boolean };
cases: { enabled: boolean };
overviewNext: { enabled: boolean };
};
}
export const plugin: PluginInitializer<

View file

@ -0,0 +1,51 @@
/*
* 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 * as PluginContext from '../../hooks/use_plugin_context';
import { PluginContextValue } from '../../context/plugin_context';
import { OverviewPage } from './';
import { OverviewPage as OldOverviewPage } from './old_overview_page';
import { OverviewPage as NewOverviewPage } from './overview_page';
describe('Overview page', () => {
it('should render the old overview page when feature flag is disabled', () => {
const pluginContext = {
config: {
unsafe: {
overviewNext: { enabled: false },
},
},
};
jest
.spyOn(PluginContext, 'usePluginContext')
.mockReturnValue(pluginContext as PluginContextValue);
const component = shallow(<OverviewPage routeParams={{ query: {} }} />);
expect(component.find(OldOverviewPage)).toHaveLength(1);
expect(component.find(NewOverviewPage)).toHaveLength(0);
});
it('should render the new overview page when feature flag is enabled', () => {
const pluginContext = {
config: {
unsafe: {
overviewNext: { enabled: true },
},
},
};
jest
.spyOn(PluginContext, 'usePluginContext')
.mockReturnValue(pluginContext as PluginContextValue);
const component = shallow(<OverviewPage routeParams={{ query: {} }} />);
expect(component.find(OldOverviewPage)).toHaveLength(0);
expect(component.find(NewOverviewPage)).toHaveLength(1);
});
});

View file

@ -4,133 +4,24 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiPanel } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useTrackPageview } from '../..';
import { EmptySections } from '../../components/app/empty_sections';
import { ObservabilityHeaderMenu } from '../../components/app/header';
import { NewsFeed } from '../../components/app/news_feed';
import { Resources } from '../../components/app/resources';
import { AlertsSection } from '../../components/app/section/alerts';
import { DatePicker } from '../../components/shared/date_picker';
import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
import { useFetcher } from '../../hooks/use_fetcher';
import { useHasData } from '../../hooks/use_has_data';
import { usePluginContext } from '../../hooks/use_plugin_context';
import { useTimeRange } from '../../hooks/use_time_range';
import { RouteParams } from '../../routes';
import { getNewsFeed } from '../../services/get_news_feed';
import { getBucketSize } from '../../utils/get_bucket_size';
import { getNoDataConfig } from '../../utils/no_data_config';
import { DataSections } from './data_sections';
import { LoadingObservability } from './loading_observability';
import { usePluginContext } from '../../hooks/use_plugin_context';
import { OverviewPage as OldOverviewPage } from './old_overview_page';
import { OverviewPage as NewOverviewPage } from './overview_page';
export type { BucketSize } from './old_overview_page';
interface Props {
routeParams: RouteParams<'/overview'>;
}
export type BucketSize = ReturnType<typeof calculateBucketSize>;
function calculateBucketSize({ start, end }: { start?: number; end?: number }) {
if (start && end) {
return getBucketSize({ start, end, minInterval: '60s' });
export function OverviewPage(props: Props) {
const { config } = usePluginContext();
if (config.unsafe.overviewNext.enabled) {
return <NewOverviewPage {...props} />;
} else {
return <OldOverviewPage {...props} />;
}
}
export function OverviewPage({ routeParams }: Props) {
useTrackPageview({ app: 'observability-overview', path: 'overview' });
useTrackPageview({ app: 'observability-overview', path: 'overview', delay: 15000 });
useBreadcrumbs([
{
text: i18n.translate('xpack.observability.breadcrumbs.overviewLinkText', {
defaultMessage: 'Overview',
}),
},
]);
const { core, ObservabilityPageTemplate } = usePluginContext();
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange();
const relativeTime = { start: relativeStart, end: relativeEnd };
const absoluteTime = { start: absoluteStart, end: absoluteEnd };
const { data: newsFeed } = useFetcher(() => getNewsFeed({ core }), [core]);
const { hasDataMap, hasAnyData, isAllRequestsComplete } = useHasData();
if (hasAnyData === undefined) {
return <LoadingObservability />;
}
const hasData = hasAnyData === true || (isAllRequestsComplete === false ? undefined : false);
const noDataConfig = getNoDataConfig({
hasData,
basePath: core.http.basePath,
docsLink: core.docLinks.links.observability.guide,
});
const { refreshInterval = 10000, refreshPaused = true } = routeParams.query;
const bucketSize = calculateBucketSize({
start: absoluteTime.start,
end: absoluteTime.end,
});
return (
<ObservabilityPageTemplate
noDataConfig={noDataConfig}
pageHeader={
hasData
? {
pageTitle: overviewPageTitle,
rightSideItems: [
<DatePicker
rangeFrom={relativeTime.start}
rangeTo={relativeTime.end}
refreshInterval={refreshInterval}
refreshPaused={refreshPaused}
/>,
],
}
: undefined
}
>
{hasData && (
<>
<ObservabilityHeaderMenu />
<EuiFlexGroup>
<EuiFlexItem grow={6}>
{/* Data sections */}
{hasAnyData && <DataSections bucketSize={bucketSize} />}
<EmptySections />
<EuiSpacer size="l" />
<EuiFlexGroup>
<EuiFlexItem>
{/* Resources / What's New sections */}
<EuiPanel hasBorder={true}>
<Resources />
<EuiSpacer size="l" />
{!!newsFeed?.items?.length && <NewsFeed items={newsFeed.items.slice(0, 5)} />}
</EuiPanel>
</EuiFlexItem>
{hasDataMap?.alert?.hasData && (
<EuiFlexItem>
<EuiPanel hasBorder={true}>
<AlertsSection />
</EuiPanel>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</>
)}
</ObservabilityPageTemplate>
);
}
const overviewPageTitle = i18n.translate('xpack.observability.overview.pageTitle', {
defaultMessage: 'Overview',
});

View file

@ -0,0 +1,136 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiPanel } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useTrackPageview } from '../..';
import { EmptySections } from '../../components/app/empty_sections';
import { ObservabilityHeaderMenu } from '../../components/app/header';
import { NewsFeed } from '../../components/app/news_feed';
import { Resources } from '../../components/app/resources';
import { AlertsSection } from '../../components/app/section/alerts';
import { DatePicker } from '../../components/shared/date_picker';
import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
import { useFetcher } from '../../hooks/use_fetcher';
import { useHasData } from '../../hooks/use_has_data';
import { usePluginContext } from '../../hooks/use_plugin_context';
import { useTimeRange } from '../../hooks/use_time_range';
import { RouteParams } from '../../routes';
import { getNewsFeed } from '../../services/get_news_feed';
import { getBucketSize } from '../../utils/get_bucket_size';
import { getNoDataConfig } from '../../utils/no_data_config';
import { DataSections } from './data_sections';
import { LoadingObservability } from './loading_observability';
interface Props {
routeParams: RouteParams<'/overview'>;
}
export type BucketSize = ReturnType<typeof calculateBucketSize>;
function calculateBucketSize({ start, end }: { start?: number; end?: number }) {
if (start && end) {
return getBucketSize({ start, end, minInterval: '60s' });
}
}
export function OverviewPage({ routeParams }: Props) {
useTrackPageview({ app: 'observability-overview', path: 'overview' });
useTrackPageview({ app: 'observability-overview', path: 'overview', delay: 15000 });
useBreadcrumbs([
{
text: i18n.translate('xpack.observability.breadcrumbs.overviewLinkText', {
defaultMessage: 'Overview',
}),
},
]);
const { core, ObservabilityPageTemplate } = usePluginContext();
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange();
const relativeTime = { start: relativeStart, end: relativeEnd };
const absoluteTime = { start: absoluteStart, end: absoluteEnd };
const { data: newsFeed } = useFetcher(() => getNewsFeed({ core }), [core]);
const { hasDataMap, hasAnyData, isAllRequestsComplete } = useHasData();
if (hasAnyData === undefined) {
return <LoadingObservability />;
}
const hasData = hasAnyData === true || (isAllRequestsComplete === false ? undefined : false);
const noDataConfig = getNoDataConfig({
hasData,
basePath: core.http.basePath,
docsLink: core.docLinks.links.observability.guide,
});
const { refreshInterval = 10000, refreshPaused = true } = routeParams.query;
const bucketSize = calculateBucketSize({
start: absoluteTime.start,
end: absoluteTime.end,
});
return (
<ObservabilityPageTemplate
noDataConfig={noDataConfig}
pageHeader={
hasData
? {
pageTitle: overviewPageTitle,
rightSideItems: [
<DatePicker
rangeFrom={relativeTime.start}
rangeTo={relativeTime.end}
refreshInterval={refreshInterval}
refreshPaused={refreshPaused}
/>,
],
}
: undefined
}
>
{hasData && (
<>
<ObservabilityHeaderMenu />
<EuiFlexGroup>
<EuiFlexItem grow={6}>
{/* Data sections */}
{hasAnyData && <DataSections bucketSize={bucketSize} />}
<EmptySections />
<EuiSpacer size="l" />
<EuiFlexGroup>
<EuiFlexItem>
{/* Resources / What's New sections */}
<EuiPanel hasBorder={true}>
<Resources />
<EuiSpacer size="l" />
{!!newsFeed?.items?.length && <NewsFeed items={newsFeed.items.slice(0, 5)} />}
</EuiPanel>
</EuiFlexItem>
{hasDataMap?.alert?.hasData && (
<EuiFlexItem>
<EuiPanel hasBorder={true}>
<AlertsSection />
</EuiPanel>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</>
)}
</ObservabilityPageTemplate>
);
}
const overviewPageTitle = i18n.translate('xpack.observability.overview.pageTitle', {
defaultMessage: 'Overview',
});

View file

@ -66,7 +66,11 @@ const withCore = makeDecorator({
setHeaderActionMenu: () => {},
} as unknown as AppMountParameters,
config: {
unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } },
unsafe: {
alertingExperience: { enabled: true },
cases: { enabled: true },
overviewNext: { enabled: false },
},
},
core: options as CoreStart,
plugins: {

View file

@ -0,0 +1,82 @@
/*
* 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 React from 'react';
import { useTrackPageview } from '../..';
import { DatePicker } from '../../components/shared/date_picker';
import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
import { useHasData } from '../../hooks/use_has_data';
import { usePluginContext } from '../../hooks/use_plugin_context';
import { useTimeRange } from '../../hooks/use_time_range';
import { RouteParams } from '../../routes';
import { getNoDataConfig } from '../../utils/no_data_config';
import { LoadingObservability } from './loading_observability';
interface Props {
routeParams: RouteParams<'/overview'>;
}
export function OverviewPage({ routeParams }: Props) {
useTrackPageview({ app: 'observability-overview', path: 'overview' });
useTrackPageview({ app: 'observability-overview', path: 'overview', delay: 15000 });
useBreadcrumbs([
{
text: i18n.translate('xpack.observability.breadcrumbs.overviewLinkText', {
defaultMessage: 'Overview',
}),
},
]);
const { core, ObservabilityPageTemplate } = usePluginContext();
const { relativeStart, relativeEnd } = useTimeRange();
const relativeTime = { start: relativeStart, end: relativeEnd };
const { hasAnyData, isAllRequestsComplete } = useHasData();
if (hasAnyData === undefined) {
return <LoadingObservability />;
}
const hasData = hasAnyData === true || (isAllRequestsComplete === false ? undefined : false);
const noDataConfig = getNoDataConfig({
hasData,
basePath: core.http.basePath,
docsLink: core.docLinks.links.observability.guide,
});
const { refreshInterval = 10000, refreshPaused = true } = routeParams.query;
return (
<ObservabilityPageTemplate
noDataConfig={noDataConfig}
pageHeader={
hasData
? {
pageTitle: overviewPageTitle,
rightSideItems: [
<DatePicker
rangeFrom={relativeTime.start}
rangeTo={relativeTime.end}
refreshInterval={refreshInterval}
refreshPaused={refreshPaused}
/>,
],
}
: undefined
}
>
{hasData && <div>New observability content goes here</div>}
</ObservabilityPageTemplate>
);
}
const overviewPageTitle = i18n.translate('xpack.observability.overview.pageTitle', {
defaultMessage: 'Overview',
});

View file

@ -34,7 +34,13 @@ export const core = {
},
} as unknown as CoreStart;
const config = { unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } } };
const config = {
unsafe: {
alertingExperience: { enabled: true },
cases: { enabled: true },
overviewNext: { enabled: false },
},
};
const plugins = {
data: { query: { timefilter: { timefilter: { setTime: jest.fn() } } } },

View file

@ -34,6 +34,7 @@ export const config: PluginConfigDescriptor = {
unsafe: schema.object({
alertingExperience: schema.object({ enabled: schema.boolean({ defaultValue: true }) }),
cases: schema.object({ enabled: schema.boolean({ defaultValue: true }) }),
overviewNext: schema.object({ enabled: schema.boolean({ defaultValue: false }) }),
}),
}),
};