mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[Stateful sidenav] Update stack management landing page (#191735)](https://github.com/elastic/kibana/pull/191735) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Sébastien Loix","email":"sebastien.loix@elastic.co"},"sourceCommit":{"committedDate":"2024-09-24T13:01:02Z","message":"[Stateful sidenav] Update stack management landing page (#191735)","sha":"92f13200194e2faf35aa95a21e95d39323b2e824","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","Team:SharedUX","backport:prev-minor","ci:project-deploy-observability","Team:obs-ux-management"],"title":"[Stateful sidenav] Update stack management landing page","number":191735,"url":"https://github.com/elastic/kibana/pull/191735","mergeCommit":{"message":"[Stateful sidenav] Update stack management landing page (#191735)","sha":"92f13200194e2faf35aa95a21e95d39323b2e824"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/191735","number":191735,"mergeCommit":{"message":"[Stateful sidenav] Update stack management landing page (#191735)","sha":"92f13200194e2faf35aa95a21e95d39323b2e824"}}]}] BACKPORT--> Co-authored-by: Sébastien Loix <sebastien.loix@elastic.co>
This commit is contained in:
parent
4686f3544c
commit
1e7765687c
32 changed files with 529 additions and 125 deletions
|
@ -568,6 +568,8 @@ export class ChromeService {
|
|||
sideNav: {
|
||||
getIsCollapsed$: () => this.isSideNavCollapsed$.asObservable(),
|
||||
setIsCollapsed: setIsSideNavCollapsed,
|
||||
getPanelSelectedNode$: projectNavigation.getPanelSelectedNode$.bind(projectNavigation),
|
||||
setPanelSelectedNode: projectNavigation.setPanelSelectedNode.bind(projectNavigation),
|
||||
},
|
||||
getActiveSolutionNavId$: () => projectNavigation.getActiveSolutionNavId$(),
|
||||
project: {
|
||||
|
|
|
@ -1003,4 +1003,69 @@ describe('solution navigations', () => {
|
|||
expect(activeSolution).toEqual(solution1);
|
||||
}
|
||||
});
|
||||
|
||||
it('should set and return the nav panel selected node', async () => {
|
||||
const { projectNavigation } = setup({ navLinkIds: ['link1', 'link2', 'link3'] });
|
||||
|
||||
{
|
||||
const selectedNode = await firstValueFrom(projectNavigation.getPanelSelectedNode$());
|
||||
expect(selectedNode).toBeNull();
|
||||
}
|
||||
|
||||
{
|
||||
const node: ChromeProjectNavigationNode = {
|
||||
id: 'node1',
|
||||
title: 'Node 1',
|
||||
path: 'node1',
|
||||
};
|
||||
projectNavigation.setPanelSelectedNode(node);
|
||||
|
||||
const selectedNode = await firstValueFrom(projectNavigation.getPanelSelectedNode$());
|
||||
|
||||
expect(selectedNode).toBe(node);
|
||||
}
|
||||
|
||||
{
|
||||
const fooSolution: SolutionNavigationDefinition<any> = {
|
||||
id: 'fooSolution',
|
||||
title: 'Foo solution',
|
||||
icon: 'logoSolution',
|
||||
homePage: 'discover',
|
||||
navigationTree$: of({
|
||||
body: [
|
||||
{
|
||||
type: 'navGroup',
|
||||
id: 'group1',
|
||||
children: [
|
||||
{ link: 'link1' },
|
||||
{
|
||||
id: 'group2',
|
||||
children: [
|
||||
{
|
||||
link: 'link2', // We'll target this node using its id
|
||||
},
|
||||
],
|
||||
},
|
||||
{ link: 'link3' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
projectNavigation.changeActiveSolutionNavigation('foo');
|
||||
projectNavigation.updateSolutionNavigations({ foo: fooSolution });
|
||||
|
||||
projectNavigation.setPanelSelectedNode('link2'); // Set the selected node using its id
|
||||
|
||||
const selectedNode = await firstValueFrom(projectNavigation.getPanelSelectedNode$());
|
||||
|
||||
expect(selectedNode).toMatchObject({
|
||||
id: 'link2',
|
||||
href: '/app/link2',
|
||||
path: 'group1.group2.link2',
|
||||
title: 'LINK2',
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -74,6 +74,10 @@ export class ProjectNavigationService {
|
|||
// The navigation tree for the Side nav UI that still contains layout information (body, footer, etc.)
|
||||
private navigationTreeUi$ = new BehaviorSubject<NavigationTreeDefinitionUI | null>(null);
|
||||
private activeNodes$ = new BehaviorSubject<ChromeProjectNavigationNode[][]>([]);
|
||||
// Keep a reference to the nav node selected when the navigation panel is opened
|
||||
private readonly panelSelectedNode$ = new BehaviorSubject<ChromeProjectNavigationNode | null>(
|
||||
null
|
||||
);
|
||||
|
||||
private projectBreadcrumbs$ = new BehaviorSubject<{
|
||||
breadcrumbs: ChromeProjectBreadcrumb[];
|
||||
|
@ -187,6 +191,8 @@ export class ProjectNavigationService {
|
|||
getActiveSolutionNavDefinition$: this.getActiveSolutionNavDefinition$.bind(this),
|
||||
/** In stateful Kibana, get the id of the active solution navigation */
|
||||
getActiveSolutionNavId$: () => this.activeSolutionNavDefinitionId$.asObservable(),
|
||||
getPanelSelectedNode$: () => this.panelSelectedNode$.asObservable(),
|
||||
setPanelSelectedNode: this.setPanelSelectedNode.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -415,6 +421,34 @@ export class ProjectNavigationService {
|
|||
}
|
||||
}
|
||||
|
||||
private setPanelSelectedNode = (_node: string | ChromeProjectNavigationNode | null) => {
|
||||
const node = typeof _node === 'string' ? this.findNodeById(_node) : _node;
|
||||
this.panelSelectedNode$.next(node);
|
||||
};
|
||||
|
||||
private findNodeById(id: string): ChromeProjectNavigationNode | null {
|
||||
const allNodes = this.navigationTree$.getValue();
|
||||
if (!allNodes) return null;
|
||||
|
||||
const find = (nodes: ChromeProjectNavigationNode[]): ChromeProjectNavigationNode | null => {
|
||||
// Recursively search for the node with the given id
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) {
|
||||
return node;
|
||||
}
|
||||
if (node.children) {
|
||||
const found = find(node.children);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return find(allNodes);
|
||||
}
|
||||
|
||||
private get http() {
|
||||
if (!this._http) {
|
||||
throw new Error('Http service not provided.');
|
||||
|
|
|
@ -54,6 +54,8 @@ const createStartContractMock = () => {
|
|||
sideNav: {
|
||||
getIsCollapsed$: jest.fn(),
|
||||
setIsCollapsed: jest.fn(),
|
||||
getPanelSelectedNode$: jest.fn(),
|
||||
setPanelSelectedNode: jest.fn(),
|
||||
},
|
||||
getBreadcrumbsAppendExtension$: jest.fn(),
|
||||
setBreadcrumbsAppendExtension: jest.fn(),
|
||||
|
|
|
@ -51,6 +51,7 @@ export type {
|
|||
GroupDefinition,
|
||||
ItemDefinition,
|
||||
PresetDefinition,
|
||||
PanelSelectedNode,
|
||||
RecentlyAccessedDefinition,
|
||||
NavigationGroupPreset,
|
||||
RootNavigationItemDefinition,
|
||||
|
|
|
@ -16,6 +16,7 @@ import type { ChromeHelpExtension } from './help_extension';
|
|||
import type { ChromeBreadcrumb, ChromeBreadcrumbsAppendExtension } from './breadcrumb';
|
||||
import type { ChromeBadge, ChromeStyle, ChromeUserBanner } from './types';
|
||||
import type { ChromeGlobalHelpExtensionMenuLink } from './help_extension';
|
||||
import type { PanelSelectedNode } from './project_navigation';
|
||||
|
||||
/**
|
||||
* ChromeStart allows plugins to customize the global chrome header UI and
|
||||
|
@ -184,6 +185,20 @@ export interface ChromeStart {
|
|||
* @param isCollapsed The collapsed state of the side nav.
|
||||
*/
|
||||
setIsCollapsed(isCollapsed: boolean): void;
|
||||
|
||||
/**
|
||||
* Get an observable of the selected nav node that opens the side nav panel.
|
||||
*/
|
||||
getPanelSelectedNode$: () => Observable<PanelSelectedNode | null>;
|
||||
|
||||
/**
|
||||
* Set the selected nav node that opens the side nav panel.
|
||||
*
|
||||
* @param node The selected nav node that opens the side nav panel. If a string is provided,
|
||||
* it will be used as the **id** of the selected nav node. If `null` is provided, the side nav panel
|
||||
* will be closed.
|
||||
*/
|
||||
setPanelSelectedNode(node: string | PanelSelectedNode | null): void;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -31,6 +31,7 @@ export type { ChromeBadge, ChromeUserBanner, ChromeStyle } from './types';
|
|||
|
||||
export type {
|
||||
ChromeProjectNavigationNode,
|
||||
PanelSelectedNode,
|
||||
AppDeepLinkId,
|
||||
AppId,
|
||||
CloudLinkId,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { ComponentType, MouseEventHandler } from 'react';
|
||||
import type { ComponentType, MouseEventHandler, ReactNode } from 'react';
|
||||
import type { Location } from 'history';
|
||||
import type { EuiSideNavItemType, EuiThemeSizes, IconType } from '@elastic/eui';
|
||||
import type { Observable } from 'rxjs';
|
||||
|
@ -247,6 +247,13 @@ export interface ChromeProjectNavigationNode extends NodeDefinitionBase {
|
|||
isElasticInternalLink?: boolean;
|
||||
}
|
||||
|
||||
export type PanelSelectedNode = Pick<
|
||||
ChromeProjectNavigationNode,
|
||||
'id' | 'children' | 'path' | 'sideNavStatus' | 'deepLink'
|
||||
> & {
|
||||
title: string | ReactNode;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export interface SideNavCompProps {
|
||||
activeNodes: ChromeProjectNavigationNode[][];
|
||||
|
|
|
@ -37,6 +37,7 @@ export const NavigationKibanaProvider: FC<PropsWithChildren<NavigationKibanaDepe
|
|||
const { basePath } = http;
|
||||
const { navigateToUrl } = core.application;
|
||||
const isSideNavCollapsed = useObservable(chrome.sideNav.getIsCollapsed$(), true);
|
||||
const selectedPanelNode = useObservable(chrome.sideNav.getPanelSelectedNode$(), null);
|
||||
|
||||
const value: NavigationServices = useMemo(
|
||||
() => ({
|
||||
|
@ -47,6 +48,8 @@ export const NavigationKibanaProvider: FC<PropsWithChildren<NavigationKibanaDepe
|
|||
activeNodes$,
|
||||
isSideNavCollapsed,
|
||||
eventTracker: new EventTracker({ reportEvent: analytics.reportEvent }),
|
||||
selectedPanelNode,
|
||||
setSelectedPanelNode: chrome.sideNav.setPanelSelectedNode,
|
||||
}),
|
||||
[
|
||||
activeNodes$,
|
||||
|
@ -55,6 +58,8 @@ export const NavigationKibanaProvider: FC<PropsWithChildren<NavigationKibanaDepe
|
|||
chrome.recentlyAccessed,
|
||||
isSideNavCollapsed,
|
||||
navigateToUrl,
|
||||
selectedPanelNode,
|
||||
chrome.sideNav.setPanelSelectedNode,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import type {
|
|||
ChromeNavLink,
|
||||
ChromeProjectNavigationNode,
|
||||
ChromeRecentlyAccessedHistoryItem,
|
||||
PanelSelectedNode,
|
||||
} from '@kbn/core-chrome-browser';
|
||||
import { EventTracker } from './analytics';
|
||||
|
||||
|
@ -38,6 +39,8 @@ export interface NavigationServices {
|
|||
activeNodes$: Observable<ChromeProjectNavigationNode[][]>;
|
||||
isSideNavCollapsed: boolean;
|
||||
eventTracker: EventTracker;
|
||||
selectedPanelNode?: PanelSelectedNode | null;
|
||||
setSelectedPanelNode?: (node: PanelSelectedNode | null) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -55,6 +58,8 @@ export interface NavigationKibanaDependencies {
|
|||
};
|
||||
sideNav: {
|
||||
getIsCollapsed$: () => Observable<boolean>;
|
||||
getPanelSelectedNode$: () => Observable<PanelSelectedNode | null>;
|
||||
setPanelSelectedNode(node: string | PanelSelectedNode | null): void;
|
||||
};
|
||||
};
|
||||
http: {
|
||||
|
|
|
@ -15,19 +15,20 @@ import React, {
|
|||
useMemo,
|
||||
useState,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import type { ChromeProjectNavigationNode } from '@kbn/core-chrome-browser';
|
||||
import type { ChromeProjectNavigationNode, PanelSelectedNode } from '@kbn/core-chrome-browser';
|
||||
|
||||
import { DefaultContent } from './default_content';
|
||||
import { ContentProvider, PanelNavNode } from './types';
|
||||
import { ContentProvider } from './types';
|
||||
|
||||
export interface PanelContext {
|
||||
isOpen: boolean;
|
||||
toggle: () => void;
|
||||
open: (navNode: PanelNavNode) => void;
|
||||
open: (navNode: PanelSelectedNode) => void;
|
||||
close: () => void;
|
||||
/** The selected node is the node in the main panel that opens the Panel */
|
||||
selectedNode: PanelNavNode | null;
|
||||
selectedNode: PanelSelectedNode | null;
|
||||
/** Handler to retrieve the component to render in the panel */
|
||||
getContent: () => React.ReactNode;
|
||||
}
|
||||
|
@ -37,29 +38,50 @@ const Context = React.createContext<PanelContext | null>(null);
|
|||
interface Props {
|
||||
contentProvider?: ContentProvider;
|
||||
activeNodes: ChromeProjectNavigationNode[][];
|
||||
selectedNode?: PanelSelectedNode | null;
|
||||
setSelectedNode?: (node: PanelSelectedNode | null) => void;
|
||||
}
|
||||
|
||||
export const PanelProvider: FC<PropsWithChildren<Props>> = ({
|
||||
children,
|
||||
contentProvider,
|
||||
activeNodes,
|
||||
selectedNode: selectedNodeProp = null,
|
||||
setSelectedNode,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedNode, setActiveNode] = useState<PanelNavNode | null>(null);
|
||||
const [selectedNode, setActiveNode] = useState<PanelSelectedNode | null>(selectedNodeProp);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setIsOpen((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const open = useCallback((navNode: PanelNavNode) => {
|
||||
setActiveNode(navNode);
|
||||
setIsOpen(true);
|
||||
}, []);
|
||||
const open = useCallback(
|
||||
(navNode: PanelSelectedNode) => {
|
||||
setActiveNode(navNode);
|
||||
setIsOpen(true);
|
||||
setSelectedNode?.(navNode);
|
||||
},
|
||||
[setSelectedNode]
|
||||
);
|
||||
|
||||
const close = useCallback(() => {
|
||||
setActiveNode(null);
|
||||
setIsOpen(false);
|
||||
}, []);
|
||||
setSelectedNode?.(null);
|
||||
}, [setSelectedNode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedNodeProp === undefined) return;
|
||||
|
||||
setActiveNode(selectedNodeProp);
|
||||
|
||||
if (selectedNodeProp) {
|
||||
setIsOpen(true);
|
||||
} else {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}, [selectedNodeProp]);
|
||||
|
||||
const getContent = useCallback(() => {
|
||||
if (!selectedNode) {
|
||||
|
|
|
@ -8,12 +8,11 @@
|
|||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
|
||||
import { ChromeProjectNavigationNode } from '@kbn/core-chrome-browser';
|
||||
import type { ChromeProjectNavigationNode, PanelSelectedNode } from '@kbn/core-chrome-browser';
|
||||
import React, { Fragment, type FC } from 'react';
|
||||
|
||||
import { PanelGroup } from './panel_group';
|
||||
import { PanelNavItem } from './panel_nav_item';
|
||||
import type { PanelNavNode } from './types';
|
||||
|
||||
function isGroupNode({ children }: Pick<ChromeProjectNavigationNode, 'children'>) {
|
||||
return children !== undefined;
|
||||
|
@ -33,7 +32,7 @@ function isItemNode({ children }: Pick<ChromeProjectNavigationNode, 'children'>)
|
|||
* @param node The current active node
|
||||
* @returns The children serialized
|
||||
*/
|
||||
function serializeChildren(node: PanelNavNode): ChromeProjectNavigationNode[] | undefined {
|
||||
function serializeChildren(node: PanelSelectedNode): ChromeProjectNavigationNode[] | undefined {
|
||||
if (!node.children) return undefined;
|
||||
|
||||
const allChildrenAreItems = node.children.every((_node) => {
|
||||
|
@ -69,7 +68,7 @@ function serializeChildren(node: PanelNavNode): ChromeProjectNavigationNode[] |
|
|||
|
||||
interface Props {
|
||||
/** The selected node is the node in the main panel that opens the Panel */
|
||||
selectedNode: PanelNavNode;
|
||||
selectedNode: PanelSelectedNode;
|
||||
}
|
||||
|
||||
export const DefaultContent: FC<Props> = ({ selectedNode }) => {
|
||||
|
|
|
@ -18,11 +18,11 @@ import {
|
|||
import React, { useCallback, type FC } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import type { PanelSelectedNode } from '@kbn/core-chrome-browser';
|
||||
import { usePanel } from './context';
|
||||
import { getNavPanelStyles, getPanelWrapperStyles } from './styles';
|
||||
import { PanelNavNode } from './types';
|
||||
|
||||
const getTestSubj = (selectedNode: PanelNavNode | null): string | undefined => {
|
||||
const getTestSubj = (selectedNode: PanelSelectedNode | null): string | undefined => {
|
||||
if (!selectedNode) return;
|
||||
|
||||
const deeplinkId = selectedNode.deepLink?.id;
|
||||
|
|
|
@ -8,13 +8,13 @@
|
|||
*/
|
||||
|
||||
import type { ReactNode, ComponentType } from 'react';
|
||||
import type { ChromeProjectNavigationNode } from '@kbn/core-chrome-browser';
|
||||
import type { ChromeProjectNavigationNode, PanelSelectedNode } from '@kbn/core-chrome-browser';
|
||||
|
||||
export interface PanelComponentProps {
|
||||
/** Handler to close the panel */
|
||||
closePanel: () => void;
|
||||
/** The node in the main panel that opens the secondary panel */
|
||||
selectedNode: PanelNavNode;
|
||||
selectedNode: PanelSelectedNode;
|
||||
/** Jagged array of active nodes that match the current URL location */
|
||||
activeNodes: ChromeProjectNavigationNode[][];
|
||||
}
|
||||
|
@ -25,10 +25,3 @@ export interface PanelContent {
|
|||
}
|
||||
|
||||
export type ContentProvider = (nodeId: string) => PanelContent | void;
|
||||
|
||||
export type PanelNavNode = Pick<
|
||||
ChromeProjectNavigationNode,
|
||||
'id' | 'children' | 'path' | 'sideNavStatus' | 'deepLink'
|
||||
> & {
|
||||
title: string | ReactNode;
|
||||
};
|
||||
|
|
|
@ -47,7 +47,7 @@ export interface Props {
|
|||
}
|
||||
|
||||
const NavigationComp: FC<Props> = ({ navigationTree$, dataTestSubj, panelContentProvider }) => {
|
||||
const { activeNodes$ } = useNavigationService();
|
||||
const { activeNodes$, selectedPanelNode, setSelectedPanelNode } = useNavigationService();
|
||||
|
||||
const activeNodes = useObservable(activeNodes$, []);
|
||||
const navigationTree = useObservable(navigationTree$, { body: [] });
|
||||
|
@ -79,7 +79,12 @@ const NavigationComp: FC<Props> = ({ navigationTree$, dataTestSubj, panelContent
|
|||
);
|
||||
|
||||
return (
|
||||
<PanelProvider activeNodes={activeNodes} contentProvider={panelContentProvider}>
|
||||
<PanelProvider
|
||||
activeNodes={activeNodes}
|
||||
contentProvider={panelContentProvider}
|
||||
selectedNode={selectedPanelNode}
|
||||
setSelectedNode={setSelectedPanelNode}
|
||||
>
|
||||
<NavigationContext.Provider value={contextValue}>
|
||||
{/* Main navigation content */}
|
||||
<EuiCollapsibleNavBeta.Body data-test-subj={dataTestSubj}>
|
||||
|
|
|
@ -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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
import React, { type FC } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiHorizontalRule } from '@elastic/eui';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
|
||||
interface Props {
|
||||
kibanaVersion: string;
|
||||
}
|
||||
|
||||
export const ClassicEmptyPrompt: FC<Props> = ({ kibanaVersion }) => {
|
||||
return (
|
||||
<KibanaPageTemplate.EmptyPrompt
|
||||
data-test-subj="managementHome"
|
||||
iconType="managementApp"
|
||||
title={
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="management.landing.header"
|
||||
defaultMessage="Welcome to Stack Management {version}"
|
||||
values={{ version: kibanaVersion }}
|
||||
/>
|
||||
</h1>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="management.landing.subhead"
|
||||
defaultMessage="Manage your indices, data views, saved objects, Kibana settings, and more."
|
||||
/>
|
||||
</p>
|
||||
<EuiHorizontalRule />
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="management.landing.text"
|
||||
defaultMessage="A complete list of apps is in the menu on the left."
|
||||
/>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { merge } from 'lodash';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { registerTestBed, AsyncTestBedConfig, TestBed } from '@kbn/test-jest-helpers';
|
||||
|
||||
import { AppContextProvider } from '../management_app/management_context';
|
||||
|
@ -45,6 +46,7 @@ export const WithAppDependencies =
|
|||
kibanaVersion: '8.10.0',
|
||||
cardsNavigationConfig: { enabled: true },
|
||||
sections: sectionsMock,
|
||||
chromeStyle: 'classic',
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -88,4 +90,32 @@ describe('Landing Page', () => {
|
|||
expect(exists('managementHome')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Empty prompt', () => {
|
||||
test('Renders the default empty prompt when chromeStyle is "classic"', async () => {
|
||||
testBed = await setupLandingPage({
|
||||
chromeStyle: 'classic',
|
||||
cardsNavigationConfig: { enabled: false },
|
||||
});
|
||||
|
||||
const { exists } = testBed;
|
||||
|
||||
expect(exists('managementHome')).toBe(true);
|
||||
});
|
||||
|
||||
test('Renders the solution empty prompt when chromeStyle is "project"', async () => {
|
||||
const coreStart = coreMock.createStart();
|
||||
|
||||
testBed = await setupLandingPage({
|
||||
chromeStyle: 'project',
|
||||
cardsNavigationConfig: { enabled: false },
|
||||
coreStart,
|
||||
});
|
||||
|
||||
const { exists } = testBed;
|
||||
|
||||
expect(exists('managementHome')).toBe(false);
|
||||
expect(exists('managementHomeSolution')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,13 +8,13 @@
|
|||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiHorizontalRule } from '@elastic/eui';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
|
||||
import { EuiPageBody } from '@elastic/eui';
|
||||
import { CardsNavigation } from '@kbn/management-cards-navigation';
|
||||
|
||||
import { useAppContext } from '../management_app/management_context';
|
||||
import { ClassicEmptyPrompt } from './classic_empty_prompt';
|
||||
import { SolutionEmptyPrompt } from './solution_empty_prompt';
|
||||
|
||||
interface ManagementLandingPageProps {
|
||||
onAppMounted: (id: string) => void;
|
||||
|
@ -25,7 +25,8 @@ export const ManagementLandingPage = ({
|
|||
setBreadcrumbs,
|
||||
onAppMounted,
|
||||
}: ManagementLandingPageProps) => {
|
||||
const { appBasePath, sections, kibanaVersion, cardsNavigationConfig } = useAppContext();
|
||||
const { appBasePath, sections, kibanaVersion, cardsNavigationConfig, chromeStyle, coreStart } =
|
||||
useAppContext();
|
||||
setBreadcrumbs();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -45,36 +46,11 @@ export const ManagementLandingPage = ({
|
|||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<KibanaPageTemplate.EmptyPrompt
|
||||
data-test-subj="managementHome"
|
||||
iconType="managementApp"
|
||||
title={
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="management.landing.header"
|
||||
defaultMessage="Welcome to Stack Management {version}"
|
||||
values={{ version: kibanaVersion }}
|
||||
/>
|
||||
</h1>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="management.landing.subhead"
|
||||
defaultMessage="Manage your indices, data views, saved objects, Kibana settings, and more."
|
||||
/>
|
||||
</p>
|
||||
<EuiHorizontalRule />
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="management.landing.text"
|
||||
defaultMessage="A complete list of apps is in the menu on the left."
|
||||
/>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
if (!chromeStyle) return null;
|
||||
|
||||
if (chromeStyle === 'project') {
|
||||
return <SolutionEmptyPrompt kibanaVersion={kibanaVersion} coreStart={coreStart} />;
|
||||
}
|
||||
|
||||
return <ClassicEmptyPrompt kibanaVersion={kibanaVersion} />;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
import React, { type FC } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiButton, EuiLink } from '@elastic/eui';
|
||||
import { type CoreStart } from '@kbn/core/public';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
|
||||
interface Props {
|
||||
kibanaVersion: string;
|
||||
coreStart: CoreStart;
|
||||
}
|
||||
|
||||
const IndicesLink: FC<{ coreStart: CoreStart }> = ({ coreStart }) => (
|
||||
<EuiLink
|
||||
href={coreStart.application.getUrlForApp('management', { path: 'data/index_management' })}
|
||||
data-test-subj="managementLinkToIndices"
|
||||
>
|
||||
{i18n.translate('management.landing.subhead.indicesLink', {
|
||||
defaultMessage: 'indices',
|
||||
})}
|
||||
</EuiLink>
|
||||
);
|
||||
|
||||
const DataViewsLink: FC<{ coreStart: CoreStart }> = ({ coreStart }) => (
|
||||
<EuiLink
|
||||
href={coreStart.application.getUrlForApp('management', { path: 'kibana/dataViews' })}
|
||||
data-test-subj="managementLinkToDataViews"
|
||||
>
|
||||
{i18n.translate('management.landing.subhead.dataViewsLink', {
|
||||
defaultMessage: 'data views',
|
||||
})}
|
||||
</EuiLink>
|
||||
);
|
||||
|
||||
const IngestPipelinesLink: FC<{ coreStart: CoreStart }> = ({ coreStart }) => (
|
||||
<EuiLink
|
||||
href={coreStart.application.getUrlForApp('management', { path: 'ingest/ingest_pipelines' })}
|
||||
data-test-subj="managementLinkToIngestPipelines"
|
||||
>
|
||||
{i18n.translate('management.landing.subhead.ingestPipelinesLink', {
|
||||
defaultMessage: 'ingest pipelines',
|
||||
})}
|
||||
</EuiLink>
|
||||
);
|
||||
|
||||
const UsersLink: FC<{ coreStart: CoreStart }> = ({ coreStart }) => (
|
||||
<EuiLink
|
||||
href={coreStart.application.getUrlForApp('management', { path: 'security/users' })}
|
||||
data-test-subj="managementLinkToUsers"
|
||||
>
|
||||
{i18n.translate('management.landing.subhead.usersLink', {
|
||||
defaultMessage: 'users',
|
||||
})}
|
||||
</EuiLink>
|
||||
);
|
||||
|
||||
export const SolutionEmptyPrompt: FC<Props> = ({ kibanaVersion, coreStart }) => {
|
||||
return (
|
||||
<KibanaPageTemplate.EmptyPrompt
|
||||
data-test-subj="managementHomeSolution"
|
||||
iconType="managementApp"
|
||||
title={
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="management.landing.solution.header"
|
||||
defaultMessage="Stack Management {version}"
|
||||
values={{ version: kibanaVersion }}
|
||||
/>
|
||||
</h1>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="management.landing.solution.subhead"
|
||||
defaultMessage="Manage your {indicesLink}, {dataViewsLink}, {ingestPipelinesLink}, {usersLink}, and more."
|
||||
values={{
|
||||
indicesLink: <IndicesLink coreStart={coreStart} />,
|
||||
dataViewsLink: <DataViewsLink coreStart={coreStart} />,
|
||||
ingestPipelinesLink: <IngestPipelinesLink coreStart={coreStart} />,
|
||||
usersLink: <UsersLink coreStart={coreStart} />,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<EuiButton
|
||||
fill
|
||||
iconType="spaces"
|
||||
onClick={() => {
|
||||
coreStart.chrome.sideNav.setPanelSelectedNode('stack_management');
|
||||
}}
|
||||
data-test-subj="viewAllStackMngtPagesButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="management.landing.solution.viewAllPagesButton"
|
||||
defaultMessage="View all pages"
|
||||
/>
|
||||
</EuiButton>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -10,7 +10,7 @@
|
|||
import './management_app.scss';
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AppMountParameters, ChromeBreadcrumb, ScopedHistory } from '@kbn/core/public';
|
||||
|
@ -21,6 +21,7 @@ import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public';
|
|||
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
||||
import { KibanaPageTemplate, KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-template';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import type { ChromeStyle } from '@kbn/core-chrome-browser';
|
||||
import { AppContextProvider } from './management_context';
|
||||
import {
|
||||
ManagementSection,
|
||||
|
@ -29,7 +30,7 @@ import {
|
|||
} from '../../utils';
|
||||
import { ManagementRouter } from './management_router';
|
||||
import { managementSidebarNav } from '../management_sidebar_nav/management_sidebar_nav';
|
||||
import { SectionsServiceStart, NavigationCardsSubject } from '../../types';
|
||||
import { SectionsServiceStart, NavigationCardsSubject, AppDependencies } from '../../types';
|
||||
|
||||
interface ManagementAppProps {
|
||||
appBasePath: string;
|
||||
|
@ -44,14 +45,17 @@ export interface ManagementAppDependencies {
|
|||
setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => void;
|
||||
isSidebarEnabled$: BehaviorSubject<boolean>;
|
||||
cardsNavigationConfig$: BehaviorSubject<NavigationCardsSubject>;
|
||||
chromeStyle$: Observable<ChromeStyle>;
|
||||
}
|
||||
|
||||
export const ManagementApp = ({ dependencies, history, appBasePath }: ManagementAppProps) => {
|
||||
const { coreStart, setBreadcrumbs, isSidebarEnabled$, cardsNavigationConfig$ } = dependencies;
|
||||
const { coreStart, setBreadcrumbs, isSidebarEnabled$, cardsNavigationConfig$, chromeStyle$ } =
|
||||
dependencies;
|
||||
const [selectedId, setSelectedId] = useState<string>('');
|
||||
const [sections, setSections] = useState<ManagementSection[]>();
|
||||
const isSidebarEnabled = useObservable(isSidebarEnabled$);
|
||||
const cardsNavigationConfig = useObservable(cardsNavigationConfig$);
|
||||
const chromeStyle = useObservable(chromeStyle$);
|
||||
|
||||
const onAppMounted = useCallback((id: string) => {
|
||||
setSelectedId(id);
|
||||
|
@ -102,11 +106,13 @@ export const ManagementApp = ({ dependencies, history, appBasePath }: Management
|
|||
}
|
||||
: undefined;
|
||||
|
||||
const contextDependencies = {
|
||||
const contextDependencies: AppDependencies = {
|
||||
appBasePath,
|
||||
sections,
|
||||
cardsNavigationConfig,
|
||||
kibanaVersion: dependencies.kibanaVersion,
|
||||
coreStart,
|
||||
chromeStyle,
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -119,6 +119,7 @@ export class ManagementPlugin
|
|||
async mount(params: AppMountParameters) {
|
||||
const { renderApp } = await import('./application');
|
||||
const [coreStart, deps] = await core.getStartServices();
|
||||
const chromeStyle$ = coreStart.chrome.getChromeStyle$();
|
||||
|
||||
return renderApp(params, {
|
||||
sections: getSectionsServiceStartPrivate(),
|
||||
|
@ -135,6 +136,7 @@ export class ManagementPlugin
|
|||
},
|
||||
isSidebarEnabled$: managementPlugin.isSidebarEnabled$,
|
||||
cardsNavigationConfig$: managementPlugin.cardsNavigationConfig$,
|
||||
chromeStyle$,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -8,10 +8,17 @@
|
|||
*/
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { ScopedHistory, Capabilities, ThemeServiceStart } from '@kbn/core/public';
|
||||
import {
|
||||
ScopedHistory,
|
||||
Capabilities,
|
||||
ThemeServiceStart,
|
||||
CoreStart,
|
||||
ChromeBreadcrumb,
|
||||
CoreTheme,
|
||||
} from '@kbn/core/public';
|
||||
import type { LocatorPublic } from '@kbn/share-plugin/common';
|
||||
import { ChromeBreadcrumb, CoreTheme } from '@kbn/core/public';
|
||||
import type { CardsNavigationComponentProps } from '@kbn/management-cards-navigation';
|
||||
import type { ChromeStyle } from '@kbn/core-chrome-browser';
|
||||
import { ManagementSection, RegisterManagementSectionArgs } from './utils';
|
||||
import type { ManagementAppLocatorParams } from '../common/locator';
|
||||
|
||||
|
@ -98,6 +105,8 @@ export interface AppDependencies {
|
|||
kibanaVersion: string;
|
||||
sections: ManagementSection[];
|
||||
cardsNavigationConfig?: NavigationCardsSubject;
|
||||
chromeStyle?: ChromeStyle;
|
||||
coreStart: CoreStart;
|
||||
}
|
||||
|
||||
export interface ConfigSchema {
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
"@kbn/shared-ux-error-boundary",
|
||||
"@kbn/deeplinks-management",
|
||||
"@kbn/react-kibana-context-render",
|
||||
"@kbn/core-chrome-browser",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
|
|
|
@ -348,6 +348,7 @@ export const getNavigationTreeDefinition = ({
|
|||
title: 'Stack',
|
||||
},
|
||||
],
|
||||
id: 'stack_management', // This id can't be changed as we use it to open the panel programmatically
|
||||
link: 'management',
|
||||
renderAs: 'panelOpener',
|
||||
spaceBefore: null,
|
||||
|
|
|
@ -272,6 +272,7 @@ export function createNavTree(pluginsStart: ObservabilityPublicPluginsStart) {
|
|||
breadcrumbStatus: 'hidden',
|
||||
children: [
|
||||
{
|
||||
id: 'stack_management', // This id can't be changed as we use it to open the panel programmatically
|
||||
link: 'management',
|
||||
title: i18n.translate('xpack.observability.obltNav.stackManagement', {
|
||||
defaultMessage: 'Stack Management',
|
||||
|
|
|
@ -236,6 +236,7 @@ export class UsersGridPage extends Component<Props, State> {
|
|||
defaultMessage="Users"
|
||||
/>
|
||||
}
|
||||
data-test-subj="securityUsersPageHeader"
|
||||
rightSideItems={
|
||||
this.props.readOnly
|
||||
? undefined
|
||||
|
|
|
@ -7,11 +7,9 @@
|
|||
|
||||
import type { Services } from '../common/services';
|
||||
import { subscribeBreadcrumbs } from './breadcrumbs';
|
||||
import { enableManagementCardsLanding } from './management_cards';
|
||||
import { initSideNavigation } from './side_navigation';
|
||||
|
||||
export const startNavigation = (services: Services) => {
|
||||
initSideNavigation(services);
|
||||
subscribeBreadcrumbs(services);
|
||||
enableManagementCardsLanding(services);
|
||||
};
|
||||
|
|
|
@ -1,52 +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 type { CardNavExtensionDefinition } from '@kbn/management-cards-navigation';
|
||||
import {
|
||||
getNavigationPropsFromId,
|
||||
SecurityPageName,
|
||||
ExternalPageName,
|
||||
} from '@kbn/security-solution-navigation';
|
||||
import { combineLatestWith } from 'rxjs';
|
||||
import type { Services } from '../common/services';
|
||||
|
||||
const SecurityManagementCards = new Map<string, CardNavExtensionDefinition['category']>([
|
||||
[ExternalPageName.visualize, 'content'],
|
||||
[ExternalPageName.maps, 'content'],
|
||||
[SecurityPageName.entityAnalyticsManagement, 'alerts'],
|
||||
[SecurityPageName.entityAnalyticsAssetClassification, 'alerts'],
|
||||
]);
|
||||
export const enableManagementCardsLanding = (services: Services) => {
|
||||
const { securitySolution, management, application, navigation } = services;
|
||||
|
||||
securitySolution
|
||||
.getNavLinks$()
|
||||
.pipe(combineLatestWith(navigation.isSolutionNavEnabled$))
|
||||
.subscribe(([navLinks, isSolutionNavEnabled]) => {
|
||||
const cardNavDefinitions = navLinks.reduce<Record<string, CardNavExtensionDefinition>>(
|
||||
(acc, navLink) => {
|
||||
if (SecurityManagementCards.has(navLink.id)) {
|
||||
const { appId, deepLinkId, path } = getNavigationPropsFromId(navLink.id);
|
||||
acc[navLink.id] = {
|
||||
category: SecurityManagementCards.get(navLink.id) ?? 'other',
|
||||
title: navLink.title,
|
||||
description: navLink.description ?? '',
|
||||
icon: navLink.landingIcon ?? '',
|
||||
href: application.getUrlForApp(appId, { deepLinkId, path }),
|
||||
skipValidation: true,
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
management.setupCardsNavigation({
|
||||
enabled: isSolutionNavEnabled,
|
||||
extendCardNavDefinitions: cardNavDefinitions,
|
||||
});
|
||||
});
|
||||
};
|
|
@ -24,7 +24,6 @@
|
|||
"@kbn/security-solution-upselling",
|
||||
"@kbn/i18n",
|
||||
"@kbn/navigation-plugin",
|
||||
"@kbn/management-cards-navigation",
|
||||
"@kbn/management-plugin",
|
||||
"@kbn/core-chrome-browser",
|
||||
]
|
||||
|
|
|
@ -13,5 +13,12 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
return {
|
||||
...functionalConfig.getAll(),
|
||||
testFiles: [require.resolve('.')],
|
||||
kbnTestServer: {
|
||||
...functionalConfig.get('kbnTestServer'),
|
||||
serverArgs: [
|
||||
...functionalConfig.get('kbnTestServer.serverArgs'),
|
||||
'--xpack.spaces.experimental.forceSolutionVisibility=true',
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -11,5 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
describe('management', function () {
|
||||
loadTestFile(require.resolve('./create_index_pattern_wizard'));
|
||||
loadTestFile(require.resolve('./feature_controls'));
|
||||
loadTestFile(require.resolve('./landing_page'));
|
||||
});
|
||||
}
|
||||
|
|
104
x-pack/test/functional/apps/management/landing_page.ts
Normal file
104
x-pack/test/functional/apps/management/landing_page.ts
Normal file
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import type { SolutionView } from '@kbn/spaces-plugin/common';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const browser = getService('browser');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const spaces = getService('spaces');
|
||||
const PageObjects = getPageObjects(['settings', 'common', 'dashboard', 'timePicker', 'header']);
|
||||
|
||||
describe('landing page', function describeIndexTests() {
|
||||
let cleanUp: () => Promise<unknown> = () => Promise.resolve();
|
||||
let spaceCreated: { id: string } = { id: '' };
|
||||
|
||||
it('should render the "classic" prompt', async function () {
|
||||
await PageObjects.common.navigateToApp('management');
|
||||
await testSubjects.existOrFail('managementHome', { timeout: 3000 });
|
||||
});
|
||||
|
||||
describe('solution empty prompt', () => {
|
||||
const createSpaceWithSolutionAndNavigateToManagement = async (solution: SolutionView) => {
|
||||
({ cleanUp, space: spaceCreated } = await spaces.create({ solution }));
|
||||
|
||||
await PageObjects.common.navigateToApp('management', { basePath: `/s/${spaceCreated.id}` });
|
||||
|
||||
return async () => {
|
||||
await cleanUp();
|
||||
cleanUp = () => Promise.resolve();
|
||||
};
|
||||
};
|
||||
|
||||
afterEach(async function afterEach() {
|
||||
await cleanUp();
|
||||
});
|
||||
|
||||
/** Test that the empty prompt has a button to open the stack managment panel */
|
||||
const testStackManagmentPanel = async () => {
|
||||
await testSubjects.missingOrFail('~sideNavPanel-id-stack_management', { timeout: 1000 });
|
||||
await testSubjects.click('~viewAllStackMngtPagesButton'); // open the side nav
|
||||
await testSubjects.existOrFail('~sideNavPanel-id-stack_management', { timeout: 3000 });
|
||||
};
|
||||
|
||||
const testCorrectEmptyPrompt = async () => {
|
||||
await testSubjects.missingOrFail('managementHome', { timeout: 3000 });
|
||||
await testSubjects.existOrFail('managementHomeSolution', { timeout: 3000 });
|
||||
};
|
||||
|
||||
it('should render the "solution" prompt when the space has a solution set', async function () {
|
||||
{
|
||||
const deleteSpace = await createSpaceWithSolutionAndNavigateToManagement('es');
|
||||
await testCorrectEmptyPrompt();
|
||||
await testStackManagmentPanel();
|
||||
await deleteSpace();
|
||||
}
|
||||
|
||||
{
|
||||
const deleteSpace = await createSpaceWithSolutionAndNavigateToManagement('oblt');
|
||||
await testCorrectEmptyPrompt();
|
||||
await testStackManagmentPanel();
|
||||
await deleteSpace();
|
||||
}
|
||||
|
||||
{
|
||||
const deleteSpace = await createSpaceWithSolutionAndNavigateToManagement('security');
|
||||
await testCorrectEmptyPrompt();
|
||||
await testStackManagmentPanel();
|
||||
await deleteSpace();
|
||||
}
|
||||
});
|
||||
|
||||
it('should have links to pages in management', async function () {
|
||||
await createSpaceWithSolutionAndNavigateToManagement('es');
|
||||
|
||||
await testSubjects.click('~managementLinkToIndices', 3000);
|
||||
await testSubjects.existOrFail('~indexManagementHeaderContent', { timeout: 3000 });
|
||||
await browser.goBack();
|
||||
await testSubjects.existOrFail('managementHomeSolution', { timeout: 3000 });
|
||||
|
||||
await testSubjects.click('~managementLinkToDataViews', 3000);
|
||||
await testSubjects.existOrFail('~indexPatternTable', { timeout: 3000 });
|
||||
await browser.goBack();
|
||||
await testSubjects.existOrFail('managementHomeSolution', { timeout: 3000 });
|
||||
|
||||
await testSubjects.click('~managementLinkToIngestPipelines', 3000);
|
||||
const appTitle = await testSubjects.getVisibleText('appTitle');
|
||||
expect(appTitle).to.be('Ingest Pipelines');
|
||||
// Note: for some reason, browser.goBack() does not work from Ingest Pipelines
|
||||
// so using navigateToApp instead;
|
||||
await PageObjects.common.navigateToApp('management', { basePath: `/s/${spaceCreated.id}` });
|
||||
await testSubjects.existOrFail('managementHomeSolution', { timeout: 3000 });
|
||||
|
||||
await testSubjects.click('~managementLinkToUsers', 3000);
|
||||
await testSubjects.existOrFail('~securityUsersPageHeader', { timeout: 3000 });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue