[Observability] Landing page for Observability (#67467)

* creating overview page and menu

* styling the home page

* adjusting breadcrumb

* renaming isnt working

* renaming isnt working

* renaming isnt working

* fixing import

* fixing scroll when resize window

* fixing eslint errors

* prepending links

* adding target option

* refactoring

* adding dark mode support

* fixing prettier format

* fixing i18n

* reverting some unnecessary changes

* addressing PR comments

* fixing functional tests

* ordering observability menu

* fixing tests

* addressing PR comments

* fixing test

* fixing scroll

* addressing pr comments

* addressing pr comments

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Cauê Marcondes 2020-06-08 08:07:35 +01:00 committed by GitHub
parent 48ef260e11
commit 07e3c9cb41
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 408 additions and 39 deletions

View file

@ -48,7 +48,8 @@
"xpack.triggersActionsUI": "plugins/triggers_actions_ui",
"xpack.upgradeAssistant": "plugins/upgrade_assistant",
"xpack.uptime": ["plugins/uptime"],
"xpack.watcher": "plugins/watcher"
"xpack.watcher": "plugins/watcher",
"xpack.observability": "plugins/observability"
},
"translations": [
"plugins/translations/translations/zh-CN.json",

View file

@ -75,7 +75,7 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
core.application.register({
id: 'apm',
title: 'APM',
order: 8100,
order: 8300,
euiIconType: 'apmApp',
appRoute: '/app/apm',
icon: 'plugins/apm/public/icon.svg',

View file

@ -64,7 +64,7 @@ export class Plugin
defaultMessage: 'Logs',
}),
euiIconType: 'logsApp',
order: 8000,
order: 8100,
appRoute: '/app/logs',
category: DEFAULT_APP_CATEGORIES.observability,
mount: async (params: AppMountParameters) => {
@ -89,7 +89,7 @@ export class Plugin
defaultMessage: 'Metrics',
}),
euiIconType: 'metricsApp',
order: 8001,
order: 8200,
appRoute: '/app/metrics',
category: DEFAULT_APP_CATEGORIES.observability,
mount: async (params: AppMountParameters) => {

View file

@ -0,0 +1,29 @@
/*
* 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 { EuiThemeProvider } from '../../../../legacy/common/eui_styled_components';
import { AppMountParameters, CoreStart } from '../../../../../src/core/public';
import { Home } from '../pages/home';
import { PluginContext } from '../context/plugin_context';
export const renderApp = (core: CoreStart, { element }: AppMountParameters) => {
const i18nCore = core.i18n;
const isDarkMode = core.uiSettings.get('theme:darkMode');
ReactDOM.render(
<PluginContext.Provider value={{ core }}>
<EuiThemeProvider darkMode={isDarkMode}>
<i18nCore.Context>
<Home />
</i18nCore.Context>
</EuiThemeProvider>
</PluginContext.Provider>,
element
);
return () => {
ReactDOM.unmountComponentAtNode(element);
};
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View file

@ -0,0 +1,14 @@
/*
* 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 { createContext } from 'react';
import { AppMountContext } from 'kibana/public';
export interface PluginContextValue {
core: AppMountContext['core'];
}
export const PluginContext = createContext({} as PluginContextValue);

View file

@ -0,0 +1,12 @@
/*
* 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 { useContext } from 'react';
import { PluginContext } from '../context/plugin_context';
export function usePluginContext() {
return useContext(PluginContext);
}

View file

@ -0,0 +1,205 @@
/*
* 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 {
EuiButton,
EuiCard,
EuiFlexGrid,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiIcon,
EuiImage,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useEffect } from 'react';
import styled from 'styled-components';
import { usePluginContext } from '../../hooks/use_plugin_context';
import { appsSection, tryItOutItemsSection } from './section';
const Container = styled.div`
min-height: calc(100vh - 48px);
background: ${(props) => props.theme.eui.euiColorEmptyShade};
`;
const Title = styled.div`
background-color: ${(props) => props.theme.eui.euiPageBackgroundColor};
border-bottom: ${(props) => props.theme.eui.euiBorderThin};
`;
const Page = styled.div`
width: 100%;
max-width: 1200px;
margin: 0 auto;
overflow: hidden;
}
`;
const EuiCardWithoutPadding = styled(EuiCard)`
padding: 0;
`;
export const Home = () => {
const { core } = usePluginContext();
useEffect(() => {
core.chrome.setBreadcrumbs([
{
text: i18n.translate('xpack.observability.home.breadcrumb.observability', {
defaultMessage: 'Observability',
}),
},
{
text: i18n.translate('xpack.observability.home.breadcrumb.gettingStarted', {
defaultMessage: 'Getting started',
}),
},
]);
}, [core]);
return (
<Container>
<Title>
<Page>
<EuiSpacer size="xxl" />
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiIcon type="logoObservability" size="xxl" />
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="m">
<h1>
{i18n.translate('xpack.observability.home.title', {
defaultMessage: 'Observability',
})}
</h1>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xxl" />
</Page>
</Title>
<Page>
<EuiSpacer size="xxl" />
<EuiFlexGroup direction="column">
{/* title and description */}
<EuiFlexItem style={{ maxWidth: '50%' }}>
<EuiTitle size="s">
<h2>
{i18n.translate('xpack.observability.home.sectionTitle', {
defaultMessage: 'Observability built on the Elastic Stack',
})}
</h2>
</EuiTitle>
<EuiSpacer size="m" />
<EuiText size="s" color="subdued">
{i18n.translate('xpack.observability.home.sectionsubtitle', {
defaultMessage:
'Bring your logs, metrics, and APM traces together at scale in a single stack so you can monitor and react to events happening anywhere in your environment.',
})}
</EuiText>
</EuiFlexItem>
{/* Apps sections */}
<EuiFlexItem>
<EuiSpacer size="s" />
<EuiFlexGroup>
<EuiFlexItem>
<EuiFlexGrid columns={2}>
{appsSection.map((app) => (
<EuiFlexItem>
<EuiCardWithoutPadding
display="plain"
layout="horizontal"
icon={<EuiIcon size="l" type={app.icon} />}
title={
<EuiTitle size="xs" className="title">
<h3>{app.title}</h3>
</EuiTitle>
}
description={app.description}
/>
</EuiFlexItem>
))}
</EuiFlexGrid>
</EuiFlexItem>
<EuiFlexItem>
<EuiImage
size="xl"
alt="observability overview image"
url={core.http.basePath.prepend(
'/plugins/observability/assets/observability_overview.png'
)}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
{/* Get started button */}
<EuiFlexItem>
<EuiFlexGroup justifyContent="center" gutterSize="none">
<EuiFlexItem grow={false}>
<EuiButton
fill
iconType="sortRight"
iconSide="right"
href={core.http.basePath.prepend('/app/home#/tutorial_directory/logging')}
>
{i18n.translate('xpack.observability.home.getStatedButton', {
defaultMessage: 'Get started',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiHorizontalRule margin="xl" />
{/* Try it out */}
<EuiFlexItem>
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h3>
{i18n.translate('xpack.observability.home.tryItOut', {
defaultMessage: 'Try it out',
})}
</h3>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
{/* Try it out sections */}
<EuiFlexItem>
<EuiFlexGroup justifyContent="center">
{tryItOutItemsSection.map((item) => (
<EuiFlexItem grow={false} key={item.id} style={{ width: '260px' }}>
<EuiCard
layout="horizontal"
icon={<EuiIcon size="l" type={item.icon} />}
title={
<EuiTitle size="xs" className="title">
<h3>{item.title}</h3>
</EuiTitle>
}
description={item.description}
target={item.target}
href={item.href}
/>
</EuiFlexItem>
))}
</EuiFlexGroup>
<EuiSpacer />
</EuiFlexItem>
</EuiFlexGroup>
</Page>
</Container>
);
};

View file

@ -0,0 +1,84 @@
/*
* 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 { i18n } from '@kbn/i18n';
interface ISection {
id: string;
title: string;
icon: string;
description: string;
href?: string;
target?: '_blank';
}
export const appsSection: ISection[] = [
{
id: 'logs',
title: i18n.translate('xpack.observability.section.apps.logs.title', {
defaultMessage: 'Logs',
}),
icon: 'logoLogging',
description: i18n.translate('xpack.observability.section.apps.logs.description', {
defaultMessage:
'The Elastic Stack (sometimes known as the ELK Stack) is the most popular open source logging platform.',
}),
},
{
id: 'apm',
title: i18n.translate('xpack.observability.section.apps.apm.title', {
defaultMessage: 'APM',
}),
icon: 'logoAPM',
description: i18n.translate('xpack.observability.section.apps.apm.description', {
defaultMessage:
'See exactly where your application is spending time so you can quickly fix issues and feel good about the code you push.',
}),
},
{
id: 'metrics',
title: i18n.translate('xpack.observability.section.apps.metrics.title', {
defaultMessage: 'Metrics',
}),
icon: 'logoMetrics',
description: i18n.translate('xpack.observability.section.apps.metrics.description', {
defaultMessage:
'Already using the Elastic Stack for logs? Add metrics in just a few steps and correlate metrics and logs in one place.',
}),
},
{
id: 'uptime',
title: i18n.translate('xpack.observability.section.apps.uptime.title', {
defaultMessage: 'Uptime',
}),
icon: 'logoUptime',
description: i18n.translate('xpack.observability.section.apps.uptime.description', {
defaultMessage:
'React to availability issues across your apps and services before they affect users.',
}),
},
];
export const tryItOutItemsSection: ISection[] = [
{
id: 'demo',
title: i18n.translate('xpack.observability.section.tryItOut.demo.title', {
defaultMessage: 'Demo Playground',
}),
icon: 'play',
description: '',
href: 'https://demo.elastic.co/',
target: '_blank',
},
{
id: 'sampleData',
title: i18n.translate('xpack.observability.section.tryItOut.sampleData.title', {
defaultMessage: 'Add sample data',
}),
icon: 'documents',
description: '',
href: '/app/home#/tutorial_directory/sampleData',
},
];

View file

@ -3,13 +3,37 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Plugin as PluginClass, PluginInitializerContext } from 'kibana/public';
import {
AppMountParameters,
CoreSetup,
DEFAULT_APP_CATEGORIES,
Plugin as PluginClass,
PluginInitializerContext,
} from '../../../../src/core/public';
export type ClientSetup = void;
export type ClientStart = void;
export class Plugin implements PluginClass {
export class Plugin implements PluginClass<ClientSetup, ClientStart> {
constructor(context: PluginInitializerContext) {}
start() {}
setup() {}
public setup(core: CoreSetup) {
core.application.register({
id: 'observability-overview',
title: 'Overview',
order: 8000,
appRoute: '/app/observability',
category: DEFAULT_APP_CATEGORIES.observability,
async mount(params: AppMountParameters<unknown>) {
// Load application bundle
const { renderApp } = await import('./application');
// Get start services
const [coreStart] = await core.getStartServices();
return renderApp(coreStart, params);
},
});
}
public start() {}
}

View file

@ -56,7 +56,7 @@ export class UptimePlugin
appRoute: '/app/uptime#/',
id: PLUGIN.ID,
euiIconType: 'uptimeApp',
order: 8900,
order: 8400,
title: PLUGIN.TITLE,
category: DEFAULT_APP_CATEGORIES.observability,
mount: async (params: AppMountParameters) => {

View file

@ -69,7 +69,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows management navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
expect(navLinks).to.eql(['Stack Management']);
expect(navLinks).to.contain('Stack Management');
});
it(`allows settings to be changed`, async () => {
@ -125,7 +125,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows Management navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
expect(navLinks).to.eql(['Stack Management']);
expect(navLinks).to.contain('Stack Management');
});
it(`does not allow settings to be changed`, async () => {
@ -177,7 +177,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows Management navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
expect(navLinks).to.eql(['Discover', 'Stack Management']);
expect(navLinks).to.contain('Stack Management');
});
it(`does not allow navigation to advanced settings; redirects to management home`, async () => {

View file

@ -60,7 +60,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows apm navlink', async () => {
const navLinks = await appsMenu.readLinks();
expect(navLinks.map((link) => link.text)).to.eql(['APM', 'Stack Management']);
expect(navLinks.map((link) => link.text)).to.contain('APM');
});
it('can navigate to APM app', async () => {
@ -109,7 +109,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows apm navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
expect(navLinks).to.eql(['APM', 'Stack Management']);
expect(navLinks).to.contain('APM');
});
it('can navigate to APM app', async () => {

View file

@ -66,7 +66,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows canvas navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
expect(navLinks).to.eql(['Canvas', 'Stack Management']);
expect(navLinks).to.contain('Canvas');
});
it(`landing page shows "Create new workpad" button`, async () => {
@ -142,7 +142,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows canvas navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
expect(navLinks).to.eql(['Canvas', 'Stack Management']);
expect(navLinks).to.contain('Canvas');
});
it(`landing page shows disabled "Create new workpad" button`, async () => {

View file

@ -63,7 +63,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows Dev Tools navlink', async () => {
const navLinks = await appsMenu.readLinks();
expect(navLinks.map((link) => link.text)).to.eql(['Dev Tools', 'Stack Management']);
expect(navLinks.map((link) => link.text)).to.contain('Dev Tools');
});
describe('console', () => {
@ -144,7 +144,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it(`shows 'Dev Tools' navlink`, async () => {
const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
expect(navLinks).to.eql(['Dev Tools', 'Stack Management']);
expect(navLinks).to.contain('Dev Tools');
});
describe('console', () => {

View file

@ -82,7 +82,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows discover navlink', async () => {
const navLinks = await appsMenu.readLinks();
expect(navLinks.map((link) => link.text)).to.eql(['Discover', 'Stack Management']);
expect(navLinks.map((link) => link.text)).to.contain('Discover');
});
it('shows save button', async () => {
@ -169,7 +169,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows discover navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
expect(navLinks).to.eql(['Discover', 'Stack Management']);
expect(navLinks).to.contain('Discover');
});
it(`doesn't show save button`, async () => {
@ -260,7 +260,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows discover navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
expect(navLinks).to.eql(['Discover', 'Stack Management']);
expect(navLinks).to.contain('Discover');
});
it(`doesn't show save button`, async () => {

View file

@ -64,7 +64,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows graph navlink', async () => {
const navLinks = await appsMenu.readLinks();
expect(navLinks.map((link) => link.text)).to.eql(['Graph', 'Stack Management']);
expect(navLinks.map((link) => link.text)).to.contain('Graph');
});
it('landing page shows "Create new graph" button', async () => {
@ -127,7 +127,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows graph navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
expect(navLinks).to.eql(['Graph', 'Stack Management']);
expect(navLinks).to.contain('Graph');
});
it('does not show a "Create new Workspace" button', async () => {

View file

@ -71,7 +71,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows management navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
expect(navLinks).to.eql(['Stack Management']);
expect(navLinks).to.contain('Stack Management');
});
it(`index pattern listing shows create button`, async () => {
@ -125,7 +125,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows management navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
expect(navLinks).to.eql(['Stack Management']);
expect(navLinks).to.contain('Stack Management');
});
it(`index pattern listing doesn't show create button`, async () => {
@ -177,7 +177,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows Management navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
expect(navLinks).to.eql(['Discover', 'Stack Management']);
expect(navLinks).to.contain('Stack Management');
});
it(`doesn't show Index Patterns in management side-nav`, async () => {

View file

@ -61,7 +61,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows metrics navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
expect(navLinks).to.eql(['Metrics', 'Stack Management']);
expect(navLinks).to.contain('Metrics');
});
describe('infrastructure landing page without data', () => {
@ -177,7 +177,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows metrics navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
expect(navLinks).to.eql(['Metrics', 'Stack Management']);
expect(navLinks).to.contain('Metrics');
});
describe('infrastructure landing page without data', () => {

View file

@ -58,7 +58,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows logs navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
expect(navLinks).to.eql(['Logs', 'Stack Management']);
expect(navLinks).to.contain('Logs');
});
describe('logs landing page without data', () => {
@ -121,7 +121,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows logs navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
expect(navLinks).to.eql(['Logs', 'Stack Management']);
expect(navLinks).to.contain('Logs');
});
describe('logs landing page without data', () => {

View file

@ -66,7 +66,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows maps navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
expect(navLinks).to.eql(['Maps', 'Stack Management']);
expect(navLinks).to.contain('Maps');
});
it(`allows a map to be created`, async () => {
@ -153,7 +153,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows Maps navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
expect(navLinks).to.eql(['Maps', 'Stack Management']);
expect(navLinks).to.contain('Maps');
});
it(`does not show create new button`, async () => {

View file

@ -60,7 +60,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows timelion navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
expect(navLinks).to.eql(['Timelion', 'Stack Management']);
expect(navLinks).to.contain('Timelion');
});
it(`allows a timelion sheet to be created`, async () => {
@ -112,7 +112,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows timelion navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
expect(navLinks).to.eql(['Timelion', 'Stack Management']);
expect(navLinks).to.contain('Timelion');
});
it(`does not allow a timelion sheet to be created`, async () => {

View file

@ -64,7 +64,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows uptime navlink', async () => {
const navLinks = await appsMenu.readLinks();
expect(navLinks.map((link) => link.text)).to.eql(['Uptime', 'Stack Management']);
expect(navLinks.map((link) => link.text)).to.contain('Uptime');
});
it('can navigate to Uptime app', async () => {
@ -115,7 +115,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows uptime navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
expect(navLinks).to.eql(['Uptime', 'Stack Management']);
expect(navLinks).to.contain('Uptime');
});
it('can navigate to Uptime app', async () => {

View file

@ -77,7 +77,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows visualize navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
expect(navLinks).to.eql(['Visualize', 'Stack Management']);
expect(navLinks).to.contain('Visualize');
});
it(`landing page shows "Create new Visualization" button`, async () => {
@ -201,7 +201,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows visualize navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
expect(navLinks).to.eql(['Visualize', 'Stack Management']);
expect(navLinks).to.contain('Visualize');
});
it(`landing page shows "Create new Visualization" button`, async () => {
@ -316,7 +316,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('shows visualize navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
expect(navLinks).to.eql(['Visualize', 'Stack Management']);
expect(navLinks).to.contain('Visualize');
});
it(`landing page shows "Create new Visualization" button`, async () => {