mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Stateful sidenav] Cleanup iteration 2 (#184367)
This commit is contained in:
parent
b4238544ae
commit
eee34b2486
30 changed files with 152 additions and 1245 deletions
|
@ -311,7 +311,6 @@ enabled:
|
|||
- x-pack/test/functional/apps/ml/short_tests/config.ts
|
||||
- x-pack/test/functional/apps/ml/stack_management_jobs/config.ts
|
||||
- x-pack/test/functional/apps/monitoring/config.ts
|
||||
- x-pack/test/functional/apps/navigation/config.ts
|
||||
- x-pack/test/functional/apps/observability_logs_explorer/config.ts
|
||||
- x-pack/test/functional/apps/dataset_quality/config.ts
|
||||
- x-pack/test/functional/apps/painless_lab/config.ts
|
||||
|
@ -325,7 +324,6 @@ enabled:
|
|||
- x-pack/test/functional/apps/search_playground/config.ts
|
||||
- x-pack/test/functional/apps/snapshot_restore/config.ts
|
||||
- x-pack/test/functional/apps/spaces/config.ts
|
||||
- x-pack/test/functional/apps/spaces/in_solution_navigation/config.ts
|
||||
- x-pack/test/functional/apps/status_page/config.ts
|
||||
- x-pack/test/functional/apps/transform/creation/index_pattern/config.ts
|
||||
- x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/config.ts
|
||||
|
|
|
@ -83,13 +83,16 @@ export class ChromeService {
|
|||
private readonly navLinks = new NavLinksService();
|
||||
private readonly recentlyAccessed = new RecentlyAccessedService();
|
||||
private readonly docTitle = new DocTitleService();
|
||||
private readonly projectNavigation = new ProjectNavigationService();
|
||||
private readonly projectNavigation: ProjectNavigationService;
|
||||
private mutationObserver: MutationObserver | undefined;
|
||||
private readonly isSideNavCollapsed$ = new BehaviorSubject<boolean>(true);
|
||||
private logger: Logger;
|
||||
private isServerless = false;
|
||||
|
||||
constructor(private readonly params: ConstructorParams) {
|
||||
this.logger = params.coreContext.logger.get('chrome-browser');
|
||||
this.isServerless = params.coreContext.env.packageInfo.buildFlavor === 'serverless';
|
||||
this.projectNavigation = new ProjectNavigationService(this.isServerless);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,28 +7,25 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui';
|
||||
import { EuiContextMenuPanel, EuiContextMenuItem, EuiButtonEmpty } from '@elastic/eui';
|
||||
import type {
|
||||
AppDeepLinkId,
|
||||
ChromeProjectBreadcrumb,
|
||||
ChromeProjectNavigationNode,
|
||||
ChromeSetProjectBreadcrumbsParams,
|
||||
ChromeBreadcrumb,
|
||||
SolutionNavigationDefinitions,
|
||||
CloudLinks,
|
||||
} from '@kbn/core-chrome-browser';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { getSolutionNavSwitcherBreadCrumb } from '../ui/solution_nav_switcher_breadcrumbs';
|
||||
|
||||
export function buildBreadcrumbs({
|
||||
projectName,
|
||||
cloudLinks,
|
||||
projectBreadcrumbs,
|
||||
activeNodes,
|
||||
chromeBreadcrumbs,
|
||||
solutionNavigations,
|
||||
isServerless,
|
||||
}: {
|
||||
projectName?: string;
|
||||
projectBreadcrumbs: {
|
||||
|
@ -38,16 +35,12 @@ export function buildBreadcrumbs({
|
|||
chromeBreadcrumbs: ChromeBreadcrumb[];
|
||||
cloudLinks: CloudLinks;
|
||||
activeNodes: ChromeProjectNavigationNode[][];
|
||||
solutionNavigations?: {
|
||||
definitions: SolutionNavigationDefinitions;
|
||||
activeId: string | null;
|
||||
onChange: (id: string) => void;
|
||||
};
|
||||
isServerless: boolean;
|
||||
}): ChromeProjectBreadcrumb[] {
|
||||
const rootCrumb = buildRootCrumb({
|
||||
projectName,
|
||||
solutionNavigations,
|
||||
cloudLinks,
|
||||
isServerless,
|
||||
});
|
||||
|
||||
if (projectBreadcrumbs.params.absolute) {
|
||||
|
@ -99,56 +92,84 @@ export function buildBreadcrumbs({
|
|||
|
||||
function buildRootCrumb({
|
||||
projectName,
|
||||
solutionNavigations,
|
||||
cloudLinks,
|
||||
isServerless,
|
||||
}: {
|
||||
projectName?: string;
|
||||
cloudLinks: CloudLinks;
|
||||
solutionNavigations?: {
|
||||
definitions: SolutionNavigationDefinitions;
|
||||
activeId: string | null;
|
||||
onChange: (id: string) => void;
|
||||
};
|
||||
isServerless: boolean;
|
||||
}): ChromeProjectBreadcrumb {
|
||||
if (solutionNavigations) {
|
||||
// if there are solution navigations, it means that we are in Kibana stateful and not
|
||||
// in serverless with projects.
|
||||
const { definitions, activeId, onChange } = solutionNavigations;
|
||||
return getSolutionNavSwitcherBreadCrumb({
|
||||
definitions,
|
||||
onChange,
|
||||
activeId,
|
||||
cloudLinks,
|
||||
});
|
||||
if (isServerless) {
|
||||
return {
|
||||
text:
|
||||
projectName ??
|
||||
i18n.translate('core.ui.primaryNav.cloud.projectLabel', {
|
||||
defaultMessage: 'Project',
|
||||
}),
|
||||
// increase the max-width of the root breadcrumb to not truncate too soon
|
||||
style: { maxWidth: '320px' },
|
||||
popoverContent: (
|
||||
<EuiContextMenuPanel
|
||||
size="s"
|
||||
items={[
|
||||
<EuiContextMenuItem key="project" href={cloudLinks.deployment?.href} icon={'gear'}>
|
||||
<FormattedMessage
|
||||
id="core.ui.primaryNav.cloud.linkToProject"
|
||||
defaultMessage="Manage project"
|
||||
/>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem key="projects" href={cloudLinks.projects?.href} icon={'grid'}>
|
||||
<FormattedMessage
|
||||
id="core.ui.primaryNav.cloud.linkToAllProjects"
|
||||
defaultMessage="View all projects"
|
||||
/>
|
||||
</EuiContextMenuItem>,
|
||||
]}
|
||||
/>
|
||||
),
|
||||
popoverProps: { panelPaddingSize: 'none' },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
text:
|
||||
projectName ??
|
||||
i18n.translate('core.ui.primaryNav.cloud.projectLabel', {
|
||||
defaultMessage: 'Project',
|
||||
}),
|
||||
// increase the max-width of the root breadcrumb to not truncate too soon
|
||||
style: { maxWidth: '320px' },
|
||||
popoverContent: (
|
||||
<EuiContextMenuPanel
|
||||
size="s"
|
||||
items={[
|
||||
<EuiContextMenuItem key="project" href={cloudLinks.deployment?.href} icon={'gear'}>
|
||||
<FormattedMessage
|
||||
id="core.ui.primaryNav.cloud.linkToProject"
|
||||
defaultMessage="Manage project"
|
||||
/>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem key="projects" href={cloudLinks.projects?.href} icon={'grid'}>
|
||||
<FormattedMessage
|
||||
id="core.ui.primaryNav.cloud.linkToAllProjects"
|
||||
defaultMessage="View all projects"
|
||||
/>
|
||||
</EuiContextMenuItem>,
|
||||
]}
|
||||
/>
|
||||
text: i18n.translate('core.ui.primaryNav.cloud.deploymentLabel', {
|
||||
defaultMessage: 'Deployment',
|
||||
}),
|
||||
'data-test-subj': 'deploymentCrumb',
|
||||
popoverContent: () => (
|
||||
<>
|
||||
{cloudLinks.deployment && (
|
||||
<EuiButtonEmpty
|
||||
href={cloudLinks.deployment.href}
|
||||
color="text"
|
||||
iconType="gear"
|
||||
data-test-subj="manageDeploymentBtn"
|
||||
>
|
||||
{i18n.translate('core.ui.primaryNav.cloud.breadCrumbDropdown.manageDeploymentLabel', {
|
||||
defaultMessage: 'Manage this deployment',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
|
||||
{cloudLinks.deployments && (
|
||||
<EuiButtonEmpty
|
||||
href={cloudLinks.deployments.href}
|
||||
color="text"
|
||||
iconType="spaces"
|
||||
data-test-subj="viewDeploymentsBtn"
|
||||
>
|
||||
{cloudLinks.deployments.title}
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
popoverProps: { panelPaddingSize: 'none' },
|
||||
popoverProps: {
|
||||
panelPaddingSize: 'm',
|
||||
zIndex: 6000,
|
||||
panelStyle: { width: 260 },
|
||||
panelProps: {
|
||||
'data-test-subj': 'deploymentLinksPanel',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -60,16 +60,18 @@ const logger = loggerMock.create();
|
|||
const setup = ({
|
||||
locationPathName = '/',
|
||||
navLinkIds,
|
||||
isServerless = true,
|
||||
}: {
|
||||
locationPathName?: string;
|
||||
navLinkIds?: Readonly<string[]>;
|
||||
isServerless?: boolean;
|
||||
} = {}) => {
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: [locationPathName],
|
||||
});
|
||||
history.replace(locationPathName);
|
||||
|
||||
const projectNavigationService = new ProjectNavigationService();
|
||||
const projectNavigationService = new ProjectNavigationService(isServerless);
|
||||
const chromeBreadcrumbs$ = new BehaviorSubject<ChromeBreadcrumb[]>([]);
|
||||
const navLinksService = getNavLinksService(navLinkIds);
|
||||
const application = {
|
||||
|
|
|
@ -93,6 +93,8 @@ export class ProjectNavigationService {
|
|||
private navigationChangeSubscription?: Subscription;
|
||||
private unlistenHistory?: () => void;
|
||||
|
||||
constructor(private isServerless: boolean) {}
|
||||
|
||||
public start({ application, navLinksService, http, chromeBreadcrumbs$, logger }: StartDeps) {
|
||||
this.application = application;
|
||||
this.navLinksService = navLinksService;
|
||||
|
@ -159,45 +161,18 @@ export class ProjectNavigationService {
|
|||
this.activeNodes$,
|
||||
chromeBreadcrumbs$,
|
||||
this.projectName$,
|
||||
this.solutionNavDefinitions$,
|
||||
this.nextSolutionNavDefinitionId$,
|
||||
this.activeSolutionNavDefinitionId$,
|
||||
this.cloudLinks$,
|
||||
]).pipe(
|
||||
map(
|
||||
([
|
||||
map(([projectBreadcrumbs, activeNodes, chromeBreadcrumbs, projectName, cloudLinks]) => {
|
||||
return buildBreadcrumbs({
|
||||
projectName,
|
||||
projectBreadcrumbs,
|
||||
activeNodes,
|
||||
chromeBreadcrumbs,
|
||||
projectName,
|
||||
solutionNavDefinitions,
|
||||
nextSolutionNavDefinitionId,
|
||||
activeSolutionNavDefinitionId,
|
||||
cloudLinks,
|
||||
]) => {
|
||||
const solutionNavigations =
|
||||
Object.keys(solutionNavDefinitions).length > 0 &&
|
||||
(nextSolutionNavDefinitionId !== null || activeSolutionNavDefinitionId !== null)
|
||||
? {
|
||||
definitions: solutionNavDefinitions,
|
||||
activeId: activeSolutionNavDefinitionId,
|
||||
onChange: (id: string) => {
|
||||
this.goToSolutionHome(id);
|
||||
this.changeActiveSolutionNavigation(id);
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return buildBreadcrumbs({
|
||||
projectName,
|
||||
projectBreadcrumbs,
|
||||
activeNodes,
|
||||
chromeBreadcrumbs,
|
||||
solutionNavigations,
|
||||
cloudLinks,
|
||||
});
|
||||
}
|
||||
)
|
||||
isServerless: this.isServerless,
|
||||
});
|
||||
})
|
||||
);
|
||||
},
|
||||
/** In stateful Kibana, get the registered solution navigations */
|
||||
|
|
|
@ -1,94 +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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import { EuiListGroup, EuiListGroupItem, EuiTitle, EuiSpacer, EuiButtonEmpty } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type {
|
||||
ChromeProjectBreadcrumb,
|
||||
SolutionNavigationDefinitions,
|
||||
CloudLinks,
|
||||
} from '@kbn/core-chrome-browser';
|
||||
|
||||
export const getSolutionNavSwitcherBreadCrumb = ({
|
||||
definitions,
|
||||
activeId,
|
||||
onChange,
|
||||
cloudLinks,
|
||||
}: {
|
||||
definitions: SolutionNavigationDefinitions;
|
||||
activeId: string | null;
|
||||
onChange: (id: string) => void;
|
||||
cloudLinks: CloudLinks;
|
||||
}): ChromeProjectBreadcrumb => {
|
||||
const text = Object.values(definitions).find(({ id }) => id === activeId)?.title;
|
||||
return {
|
||||
text,
|
||||
'data-test-subj': 'solutionNavSwitcher',
|
||||
popoverContent: (closePopover) => (
|
||||
<>
|
||||
<EuiTitle size="xxxs">
|
||||
<h3>
|
||||
{i18n.translate('core.ui.primaryNav.cloud.breadCrumbDropdown.title', {
|
||||
defaultMessage: 'Solution view',
|
||||
})}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiListGroup bordered size="s">
|
||||
{Object.values(definitions).map(({ id, title, icon = 'gear' }) => [
|
||||
<EuiListGroupItem
|
||||
key={id}
|
||||
label={title}
|
||||
isActive={id === activeId}
|
||||
iconType={icon as string}
|
||||
data-test-subj={`solutionNavSwitcher-${id}`}
|
||||
onClick={() => {
|
||||
onChange(id);
|
||||
closePopover();
|
||||
}}
|
||||
/>,
|
||||
])}
|
||||
</EuiListGroup>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
{cloudLinks.deployment && (
|
||||
<EuiButtonEmpty
|
||||
href={cloudLinks.deployment.href}
|
||||
color="text"
|
||||
iconType="gear"
|
||||
data-test-subj="manageDeploymentBtn"
|
||||
>
|
||||
{i18n.translate('core.ui.primaryNav.cloud.breadCrumbDropdown.manageDeploymentLabel', {
|
||||
defaultMessage: 'Manage this deployment',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
|
||||
{cloudLinks.deployments && (
|
||||
<EuiButtonEmpty
|
||||
href={cloudLinks.deployments.href}
|
||||
color="text"
|
||||
iconType="spaces"
|
||||
data-test-subj="viewDeploymentsBtn"
|
||||
>
|
||||
{cloudLinks.deployments.title}
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
popoverProps: {
|
||||
panelPaddingSize: 'm',
|
||||
zIndex: 6000,
|
||||
panelStyle: { width: 260 },
|
||||
panelProps: {
|
||||
'data-test-subj': 'solutionNavSwitcherPanel',
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
|
@ -7,5 +7,3 @@
|
|||
*/
|
||||
|
||||
export const SOLUTION_NAV_FEATURE_FLAG_NAME = 'solutionNavEnabled';
|
||||
|
||||
export const ENABLE_SOLUTION_NAV_UI_SETTING_ID = 'solutionNav:enable';
|
||||
|
|
|
@ -6,4 +6,4 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { SOLUTION_NAV_FEATURE_FLAG_NAME, ENABLE_SOLUTION_NAV_UI_SETTING_ID } from './constants';
|
||||
export { SOLUTION_NAV_FEATURE_FLAG_NAME } from './constants';
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"id": "navigation",
|
||||
"server": true,
|
||||
"browser": true,
|
||||
"optionalPlugins": ["cloud","security", "cloudExperiments"],
|
||||
"optionalPlugins": ["cloud", "cloudExperiments"],
|
||||
"requiredPlugins": ["unifiedSearch"],
|
||||
"requiredBundles": []
|
||||
}
|
||||
|
|
|
@ -6,18 +6,15 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { firstValueFrom, of } from 'rxjs';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks';
|
||||
import { securityMock } from '@kbn/security-plugin/public/mocks';
|
||||
import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
|
||||
import { cloudExperimentsMock } from '@kbn/cloud-experiments-plugin/common/mocks';
|
||||
import type { BuildFlavor } from '@kbn/config';
|
||||
import type { UserSettingsData } from '@kbn/user-profile-components';
|
||||
import { ENABLE_SOLUTION_NAV_UI_SETTING_ID, SOLUTION_NAV_FEATURE_FLAG_NAME } from '../common';
|
||||
import { SOLUTION_NAV_FEATURE_FLAG_NAME } from '../common';
|
||||
import { NavigationPublicPlugin } from './plugin';
|
||||
import { ConfigSchema } from './types';
|
||||
import { SolutionNavUserProfileToggle } from './solution_nav_userprofile_toggle';
|
||||
|
||||
jest.mock('rxjs', () => {
|
||||
const original = jest.requireActual('rxjs');
|
||||
|
@ -27,57 +24,34 @@ jest.mock('rxjs', () => {
|
|||
};
|
||||
});
|
||||
|
||||
const defaultConfig: ConfigSchema['solutionNavigation'] = {
|
||||
enabled: true,
|
||||
defaultSolution: 'es',
|
||||
};
|
||||
|
||||
const setup = (
|
||||
partialConfig: Partial<ConfigSchema['solutionNavigation']> & {
|
||||
config: {
|
||||
featureOn: boolean;
|
||||
},
|
||||
{
|
||||
buildFlavor = 'traditional',
|
||||
userSettings = {},
|
||||
uiSettingsValues,
|
||||
}: {
|
||||
buildFlavor?: BuildFlavor;
|
||||
userSettings?: UserSettingsData;
|
||||
uiSettingsValues?: Record<string, any>;
|
||||
} = {}
|
||||
) => {
|
||||
const config = {
|
||||
solutionNavigation: {
|
||||
...defaultConfig,
|
||||
...partialConfig,
|
||||
},
|
||||
};
|
||||
|
||||
const initializerContext = coreMock.createPluginInitializerContext(config, { buildFlavor });
|
||||
const initializerContext = coreMock.createPluginInitializerContext({}, { buildFlavor });
|
||||
const plugin = new NavigationPublicPlugin(initializerContext);
|
||||
|
||||
const setChromeStyle = jest.fn();
|
||||
const coreStart = coreMock.createStart();
|
||||
const unifiedSearch = unifiedSearchPluginMock.createStartContract();
|
||||
const cloud = cloudMock.createStart();
|
||||
const security = securityMock.createStart();
|
||||
const cloudExperiments = cloudExperimentsMock.createStartMock();
|
||||
cloudExperiments.getVariation.mockImplementation((key) => {
|
||||
if (key === SOLUTION_NAV_FEATURE_FLAG_NAME) {
|
||||
return Promise.resolve(partialConfig.featureOn);
|
||||
return Promise.resolve(config.featureOn);
|
||||
}
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
|
||||
security.userProfiles.userProfileLoaded$ = of(true);
|
||||
security.userProfiles.userProfile$ = of({ userSettings });
|
||||
|
||||
const getGlobalSetting$ = jest.fn();
|
||||
if (uiSettingsValues) {
|
||||
getGlobalSetting$.mockImplementation((settingId: string) =>
|
||||
of(uiSettingsValues[settingId] ?? 'unknown')
|
||||
);
|
||||
}
|
||||
const settingsGlobalClient = {
|
||||
...coreStart.settings.globalClient,
|
||||
get$: getGlobalSetting$,
|
||||
|
@ -90,7 +64,6 @@ const setup = (
|
|||
coreStart,
|
||||
unifiedSearch,
|
||||
cloud,
|
||||
security,
|
||||
cloudExperiments,
|
||||
config,
|
||||
setChromeStyle,
|
||||
|
@ -120,35 +93,24 @@ describe('Navigation Plugin', () => {
|
|||
describe('feature flag enabled', () => {
|
||||
const featureOn = true;
|
||||
|
||||
it('should add the opt in/out toggle in the user menu', async () => {
|
||||
const uiSettingsValues: Record<string, any> = {
|
||||
[ENABLE_SOLUTION_NAV_UI_SETTING_ID]: true,
|
||||
};
|
||||
// TODO: this test will have to be updated when we will read the space state
|
||||
it.skip('should add the default solution navs **and** set the active nav', async () => {
|
||||
const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments } = setup({ featureOn });
|
||||
|
||||
const { plugin, coreStart, unifiedSearch, cloud, security, cloudExperiments } = setup(
|
||||
{
|
||||
featureOn,
|
||||
},
|
||||
{ uiSettingsValues }
|
||||
);
|
||||
|
||||
plugin.start(coreStart, { unifiedSearch, cloud, security, cloudExperiments });
|
||||
plugin.start(coreStart, { unifiedSearch, cloud, cloudExperiments });
|
||||
await new Promise((resolve) => setTimeout(resolve));
|
||||
|
||||
expect(security.navControlService.addUserMenuLinks).toHaveBeenCalled();
|
||||
const [menuLink] = security.navControlService.addUserMenuLinks.mock.calls[0][0];
|
||||
expect((menuLink.content as any)?.type).toBe(SolutionNavUserProfileToggle);
|
||||
expect(coreStart.chrome.project.updateSolutionNavigations).toHaveBeenCalled();
|
||||
|
||||
expect(coreStart.chrome.project.changeActiveSolutionNavigation).toHaveBeenCalledWith(
|
||||
'security'
|
||||
);
|
||||
});
|
||||
|
||||
describe('set Chrome style', () => {
|
||||
it('should set the Chrome style to "classic" when the feature is not enabled', async () => {
|
||||
const uiSettingsValues: Record<string, any> = {
|
||||
[ENABLE_SOLUTION_NAV_UI_SETTING_ID]: true,
|
||||
};
|
||||
|
||||
const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments } = setup(
|
||||
{ featureOn: false }, // feature not enabled
|
||||
{ uiSettingsValues }
|
||||
{ featureOn: false } // feature not enabled
|
||||
);
|
||||
|
||||
plugin.start(coreStart, { unifiedSearch, cloud, cloudExperiments });
|
||||
|
@ -156,29 +118,10 @@ describe('Navigation Plugin', () => {
|
|||
expect(coreStart.chrome.setChromeStyle).toHaveBeenCalledWith('classic');
|
||||
});
|
||||
|
||||
it('should set the Chrome style to "classic" when the feature is enabled BUT globalSettings is disabled', async () => {
|
||||
const uiSettingsValues: Record<string, any> = {
|
||||
[ENABLE_SOLUTION_NAV_UI_SETTING_ID]: false, // Global setting disabled
|
||||
};
|
||||
|
||||
it('should NOT set the Chrome style when the feature is enabled BUT on serverless', async () => {
|
||||
const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments } = setup(
|
||||
{ featureOn: true }, // feature enabled
|
||||
{ uiSettingsValues }
|
||||
);
|
||||
|
||||
plugin.start(coreStart, { unifiedSearch, cloud, cloudExperiments });
|
||||
await new Promise((resolve) => setTimeout(resolve));
|
||||
expect(coreStart.chrome.setChromeStyle).toHaveBeenCalledWith('classic');
|
||||
});
|
||||
|
||||
it('should NOT set the Chrome style when the feature is enabled, globalSettings is enabled BUT on serverless', async () => {
|
||||
const uiSettingsValues: Record<string, any> = {
|
||||
[ENABLE_SOLUTION_NAV_UI_SETTING_ID]: true, // Global setting enabled
|
||||
};
|
||||
|
||||
const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments } = setup(
|
||||
{ featureOn: true }, // feature enabled
|
||||
{ uiSettingsValues, buildFlavor: 'serverless' }
|
||||
{ buildFlavor: 'serverless' }
|
||||
);
|
||||
|
||||
plugin.start(coreStart, { unifiedSearch, cloud, cloudExperiments });
|
||||
|
@ -186,15 +129,11 @@ describe('Navigation Plugin', () => {
|
|||
expect(coreStart.chrome.setChromeStyle).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set the Chrome style to "project" when the feature is enabled', async () => {
|
||||
const uiSettingsValues: Record<string, any> = {
|
||||
[ENABLE_SOLUTION_NAV_UI_SETTING_ID]: true,
|
||||
};
|
||||
|
||||
const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments } = setup(
|
||||
{ featureOn: true },
|
||||
{ uiSettingsValues }
|
||||
);
|
||||
// TODO: this test will have to be updated when we will read the space state
|
||||
it.skip('should set the Chrome style to "project" when the feature is enabled', async () => {
|
||||
const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments } = setup({
|
||||
featureOn: true,
|
||||
});
|
||||
|
||||
plugin.start(coreStart, { unifiedSearch, cloud, cloudExperiments });
|
||||
await new Promise((resolve) => setTimeout(resolve));
|
||||
|
@ -203,145 +142,10 @@ describe('Navigation Plugin', () => {
|
|||
});
|
||||
|
||||
describe('isSolutionNavEnabled$', () => {
|
||||
describe('user has not opted in or out (undefined)', () => {
|
||||
const testCases: Array<[Record<string, any>, string, boolean]> = [
|
||||
[
|
||||
{
|
||||
[ENABLE_SOLUTION_NAV_UI_SETTING_ID]: true,
|
||||
},
|
||||
'should be enabled',
|
||||
true,
|
||||
],
|
||||
[
|
||||
{
|
||||
[ENABLE_SOLUTION_NAV_UI_SETTING_ID]: false, // feature not enabled
|
||||
},
|
||||
'should not be enabled',
|
||||
false,
|
||||
],
|
||||
];
|
||||
|
||||
testCases.forEach(([uiSettingsValues, description, expected]) => {
|
||||
it(description, async () => {
|
||||
const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments } = setup(
|
||||
{
|
||||
featureOn,
|
||||
},
|
||||
{
|
||||
userSettings: {
|
||||
// user has not opted in or out
|
||||
solutionNavOptOut: undefined,
|
||||
},
|
||||
uiSettingsValues,
|
||||
}
|
||||
);
|
||||
|
||||
const { isSolutionNavEnabled$ } = plugin.start(coreStart, {
|
||||
unifiedSearch,
|
||||
cloud,
|
||||
cloudExperiments,
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve));
|
||||
|
||||
expect(await firstValueFrom(isSolutionNavEnabled$)).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('user has opted in', () => {
|
||||
const testCases: Array<[Record<string, any>, string, boolean]> = [
|
||||
[
|
||||
{
|
||||
[ENABLE_SOLUTION_NAV_UI_SETTING_ID]: true,
|
||||
},
|
||||
'should be enabled',
|
||||
true,
|
||||
],
|
||||
[
|
||||
{
|
||||
[ENABLE_SOLUTION_NAV_UI_SETTING_ID]: false, // feature not enabled
|
||||
},
|
||||
'should not be enabled',
|
||||
false,
|
||||
],
|
||||
];
|
||||
|
||||
testCases.forEach(([uiSettingsValues, description, expected]) => {
|
||||
it(description, async () => {
|
||||
const { plugin, coreStart, unifiedSearch, cloud, security, cloudExperiments } = setup(
|
||||
{
|
||||
featureOn,
|
||||
},
|
||||
{
|
||||
userSettings: {
|
||||
// user has opted in
|
||||
solutionNavOptOut: false,
|
||||
},
|
||||
uiSettingsValues,
|
||||
}
|
||||
);
|
||||
|
||||
const { isSolutionNavEnabled$ } = plugin.start(coreStart, {
|
||||
security,
|
||||
unifiedSearch,
|
||||
cloud,
|
||||
cloudExperiments,
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve));
|
||||
|
||||
expect(await firstValueFrom(isSolutionNavEnabled$)).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('user has opted out', () => {
|
||||
const testCases: Array<[Record<string, any>, string, boolean]> = [
|
||||
[
|
||||
{
|
||||
[ENABLE_SOLUTION_NAV_UI_SETTING_ID]: true,
|
||||
},
|
||||
'should not be enabled',
|
||||
false,
|
||||
],
|
||||
[
|
||||
{
|
||||
[ENABLE_SOLUTION_NAV_UI_SETTING_ID]: false, // feature not enabled
|
||||
},
|
||||
'should not be enabled',
|
||||
false,
|
||||
],
|
||||
];
|
||||
|
||||
testCases.forEach(([uiSettingsValues, description, expected]) => {
|
||||
it(description, async () => {
|
||||
const { plugin, coreStart, unifiedSearch, cloud, security, cloudExperiments } = setup(
|
||||
{
|
||||
featureOn,
|
||||
},
|
||||
{ userSettings: { solutionNavOptOut: true }, uiSettingsValues } // user has opted out
|
||||
);
|
||||
|
||||
const { isSolutionNavEnabled$ } = plugin.start(coreStart, {
|
||||
security,
|
||||
unifiedSearch,
|
||||
cloud,
|
||||
cloudExperiments,
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve));
|
||||
|
||||
expect(await firstValueFrom(isSolutionNavEnabled$)).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('on serverless should flag must be disabled', async () => {
|
||||
const uiSettingsValues: Record<string, any> = {
|
||||
[ENABLE_SOLUTION_NAV_UI_SETTING_ID]: true, // enabled, but we are on serverless
|
||||
};
|
||||
|
||||
const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments } = setup(
|
||||
{ featureOn },
|
||||
{ buildFlavor: 'serverless', uiSettingsValues }
|
||||
{ buildFlavor: 'serverless' }
|
||||
);
|
||||
|
||||
const { isSolutionNavEnabled$ } = plugin.start(coreStart, {
|
||||
|
|
|
@ -6,45 +6,23 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
import React from 'react';
|
||||
import {
|
||||
combineLatest,
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
firstValueFrom,
|
||||
from,
|
||||
map,
|
||||
Observable,
|
||||
of,
|
||||
ReplaySubject,
|
||||
shareReplay,
|
||||
skipWhile,
|
||||
switchMap,
|
||||
take,
|
||||
takeUntil,
|
||||
} from 'rxjs';
|
||||
import { firstValueFrom, from, of, ReplaySubject, shareReplay, take } from 'rxjs';
|
||||
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
|
||||
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||
import type { SecurityPluginStart, UserMenuLink } from '@kbn/security-plugin/public';
|
||||
import type { SolutionNavigationDefinition } from '@kbn/core-chrome-browser';
|
||||
import { InternalChromeStart } from '@kbn/core-chrome-browser-internal';
|
||||
import type { PanelContentProvider } from '@kbn/shared-ux-chrome-navigation';
|
||||
import { UserProfileData } from '@kbn/user-profile-components';
|
||||
import { ENABLE_SOLUTION_NAV_UI_SETTING_ID, SOLUTION_NAV_FEATURE_FLAG_NAME } from '../common';
|
||||
import { SOLUTION_NAV_FEATURE_FLAG_NAME } from '../common';
|
||||
import type {
|
||||
NavigationPublicSetup,
|
||||
NavigationPublicStart,
|
||||
NavigationPublicSetupDependencies,
|
||||
NavigationPublicStartDependencies,
|
||||
ConfigSchema,
|
||||
AddSolutionNavigationArg,
|
||||
SolutionType,
|
||||
} from './types';
|
||||
import { TopNavMenuExtensionsRegistry, createTopNav } from './top_nav_menu';
|
||||
import { RegisteredTopNavMenuData } from './top_nav_menu/top_nav_menu_data';
|
||||
import { SideNavComponent } from './side_navigation';
|
||||
import { SolutionNavUserProfileToggle } from './solution_nav_userprofile_toggle';
|
||||
|
||||
const DEFAULT_OPT_OUT_NEW_NAV = false;
|
||||
|
||||
export class NavigationPublicPlugin
|
||||
implements
|
||||
|
@ -60,11 +38,9 @@ export class NavigationPublicPlugin
|
|||
private readonly stop$ = new ReplaySubject<void>(1);
|
||||
private coreStart?: CoreStart;
|
||||
private depsStart?: NavigationPublicStartDependencies;
|
||||
private isSolutionNavEnabled$ = of(false);
|
||||
private userProfileOptOut$: Observable<boolean | undefined> = of(undefined);
|
||||
private userProfileMenuItemAdded = false;
|
||||
private isSolutionNavExperiementEnabled$ = of(false);
|
||||
|
||||
constructor(private initializerContext: PluginInitializerContext<ConfigSchema>) {}
|
||||
constructor(private initializerContext: PluginInitializerContext) {}
|
||||
|
||||
public setup(_core: CoreSetup): NavigationPublicSetup {
|
||||
return {
|
||||
|
@ -81,25 +57,10 @@ export class NavigationPublicPlugin
|
|||
this.coreStart = core;
|
||||
this.depsStart = depsStart;
|
||||
|
||||
const { unifiedSearch, cloud, security, cloudExperiments } = depsStart;
|
||||
const { unifiedSearch, cloud, cloudExperiments } = depsStart;
|
||||
const extensions = this.topNavMenuExtensionsRegistry.getAll();
|
||||
const chrome = core.chrome as InternalChromeStart;
|
||||
|
||||
if (security) {
|
||||
this.userProfileOptOut$ = security.userProfiles.userProfileLoaded$.pipe(
|
||||
skipWhile((loaded) => {
|
||||
return !loaded;
|
||||
}),
|
||||
switchMap(() => {
|
||||
return security.userProfiles.userProfile$ as Observable<UserProfileData>;
|
||||
}),
|
||||
map((profile) => {
|
||||
return profile?.userSettings?.solutionNavOptOut;
|
||||
}),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
*
|
||||
* This helps clients of navigation to create
|
||||
|
@ -120,60 +81,22 @@ export class NavigationPublicPlugin
|
|||
return createTopNav(customUnifiedSearch ?? unifiedSearch, customExtensions ?? extensions);
|
||||
};
|
||||
|
||||
const config = this.initializerContext.config.get();
|
||||
const {
|
||||
solutionNavigation: { defaultSolution },
|
||||
} = config;
|
||||
|
||||
const onCloud = cloud !== undefined; // The new side nav will initially only be available to cloud users
|
||||
const isServerless = this.initializerContext.env.packageInfo.buildFlavor === 'serverless';
|
||||
|
||||
let isSolutionNavExperiementEnabled$ = of(false);
|
||||
this.isSolutionNavEnabled$ = of(false);
|
||||
|
||||
if (cloudExperiments) {
|
||||
isSolutionNavExperiementEnabled$ =
|
||||
!onCloud || isServerless
|
||||
? of(false)
|
||||
: from(
|
||||
cloudExperiments
|
||||
.getVariation(SOLUTION_NAV_FEATURE_FLAG_NAME, false)
|
||||
.catch(() => false)
|
||||
).pipe(shareReplay(1));
|
||||
|
||||
this.isSolutionNavEnabled$ = isSolutionNavExperiementEnabled$.pipe(
|
||||
switchMap((isFeatureEnabled) => {
|
||||
return !isFeatureEnabled
|
||||
? of(false)
|
||||
: combineLatest([
|
||||
core.settings.globalClient.get$<boolean>(ENABLE_SOLUTION_NAV_UI_SETTING_ID),
|
||||
this.userProfileOptOut$,
|
||||
]).pipe(
|
||||
takeUntil(this.stop$),
|
||||
debounceTime(10),
|
||||
map(([enabled, userOptedOut]) => {
|
||||
if (!enabled || userOptedOut === true) return false;
|
||||
return true;
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
if (cloudExperiments && onCloud && !isServerless) {
|
||||
this.isSolutionNavExperiementEnabled$ = from(
|
||||
cloudExperiments.getVariation(SOLUTION_NAV_FEATURE_FLAG_NAME, false).catch(() => false)
|
||||
).pipe(shareReplay(1));
|
||||
}
|
||||
|
||||
this.isSolutionNavEnabled$
|
||||
.pipe(takeUntil(this.stop$), distinctUntilChanged())
|
||||
.subscribe((isSolutionNavEnabled) => {
|
||||
if (isServerless) return; // Serverless already controls the chrome style
|
||||
|
||||
chrome.setChromeStyle(isSolutionNavEnabled ? 'project' : 'classic');
|
||||
});
|
||||
|
||||
// Initialize the solution navigation if it is enabled
|
||||
isSolutionNavExperiementEnabled$.pipe(take(1)).subscribe((isEnabled) => {
|
||||
this.isSolutionNavExperiementEnabled$.pipe(take(1)).subscribe((isEnabled) => {
|
||||
this.initiateChromeStyleAndSideNav(chrome, { isFeatureEnabled: isEnabled, isServerless });
|
||||
|
||||
if (!isEnabled) return;
|
||||
|
||||
chrome.project.setCloudUrls(cloud!);
|
||||
this.susbcribeToSolutionNavUiSettings({ core, security, defaultSolution });
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -183,12 +106,12 @@ export class NavigationPublicPlugin
|
|||
createTopNavWithCustomContext: createCustomTopNav,
|
||||
},
|
||||
addSolutionNavigation: (solutionNavigation) => {
|
||||
firstValueFrom(isSolutionNavExperiementEnabled$).then((isEnabled) => {
|
||||
firstValueFrom(this.isSolutionNavExperiementEnabled$).then((isEnabled) => {
|
||||
if (!isEnabled) return;
|
||||
this.addSolutionNavigation(solutionNavigation);
|
||||
});
|
||||
},
|
||||
isSolutionNavEnabled$: this.isSolutionNavEnabled$,
|
||||
isSolutionNavEnabled$: this.isSolutionNavExperiementEnabled$,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -196,40 +119,6 @@ export class NavigationPublicPlugin
|
|||
this.stop$.next();
|
||||
}
|
||||
|
||||
private susbcribeToSolutionNavUiSettings({
|
||||
core,
|
||||
security,
|
||||
defaultSolution,
|
||||
}: {
|
||||
core: CoreStart;
|
||||
defaultSolution: SolutionType;
|
||||
security?: SecurityPluginStart;
|
||||
}) {
|
||||
const chrome = core.chrome as InternalChromeStart;
|
||||
|
||||
combineLatest([
|
||||
core.settings.globalClient.get$<boolean>(ENABLE_SOLUTION_NAV_UI_SETTING_ID),
|
||||
this.userProfileOptOut$,
|
||||
])
|
||||
.pipe(takeUntil(this.stop$), debounceTime(10))
|
||||
.subscribe(([enabled, userOptedOut]) => {
|
||||
if (enabled) {
|
||||
// Add menu item in the user profile menu to opt in/out of the new navigation
|
||||
this.addOptInOutUserProfile({ core, security, userOptedOut });
|
||||
} else {
|
||||
// TODO. Remove the user profile menu item if the feature is disabled.
|
||||
// But first let's wait as maybe there will be a page refresh when opting out.
|
||||
}
|
||||
|
||||
if (!enabled || userOptedOut === true) {
|
||||
chrome.project.changeActiveSolutionNavigation(null);
|
||||
chrome.setChromeStyle('classic');
|
||||
} else {
|
||||
chrome.project.changeActiveSolutionNavigation(defaultSolution);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getSideNavComponent({
|
||||
dataTestSubj,
|
||||
panelContentProvider,
|
||||
|
@ -263,34 +152,27 @@ export class NavigationPublicPlugin
|
|||
});
|
||||
}
|
||||
|
||||
private addOptInOutUserProfile({
|
||||
core,
|
||||
security,
|
||||
userOptedOut,
|
||||
}: {
|
||||
core: CoreStart;
|
||||
userOptedOut?: boolean;
|
||||
security?: SecurityPluginStart;
|
||||
}) {
|
||||
if (!security || this.userProfileMenuItemAdded) return;
|
||||
|
||||
const defaultOptOutValue = userOptedOut !== undefined ? userOptedOut : DEFAULT_OPT_OUT_NEW_NAV;
|
||||
|
||||
const menuLink: UserMenuLink = {
|
||||
content: (
|
||||
<SolutionNavUserProfileToggle
|
||||
core={core}
|
||||
security={security}
|
||||
defaultOptOutValue={defaultOptOutValue}
|
||||
/>
|
||||
),
|
||||
order: 500,
|
||||
label: '',
|
||||
iconType: '',
|
||||
href: '',
|
||||
private initiateChromeStyleAndSideNav(
|
||||
chrome: InternalChromeStart,
|
||||
{ isFeatureEnabled, isServerless }: { isFeatureEnabled: boolean; isServerless: boolean }
|
||||
) {
|
||||
// Here we will read the space state and decide if we are in classic or project style
|
||||
const mockSpaceState: { solutionView?: 'classic' | 'es' | 'oblt' | 'security' } = {
|
||||
solutionView: 'security', // Change this value to test different solution views
|
||||
};
|
||||
|
||||
security.navControlService.addUserMenuLinks([menuLink]);
|
||||
this.userProfileMenuItemAdded = true;
|
||||
const isProjectNav =
|
||||
isFeatureEnabled &&
|
||||
Boolean(mockSpaceState.solutionView) &&
|
||||
mockSpaceState.solutionView !== 'classic';
|
||||
|
||||
// On serverless the chrome style is already set by the serverless plugin
|
||||
if (!isServerless) {
|
||||
chrome.setChromeStyle(isProjectNav ? 'project' : 'classic');
|
||||
}
|
||||
|
||||
if (isProjectNav && mockSpaceState.solutionView) {
|
||||
chrome.project.changeActiveSolutionNavigation(mockSpaceState.solutionView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { SolutionNavUserProfileToggle } from './solution_nav_userprofile_toggle';
|
|
@ -1,86 +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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { of } from 'rxjs';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { securityMock } from '@kbn/security-plugin/public/mocks';
|
||||
|
||||
import { SolutionNavUserProfileToggle } from './solution_nav_userprofile_toggle';
|
||||
|
||||
const mockUseUpdateUserProfile = jest.fn();
|
||||
|
||||
jest.mock('@kbn/user-profile-components', () => {
|
||||
const original = jest.requireActual('@kbn/user-profile-components');
|
||||
return {
|
||||
...original,
|
||||
useUpdateUserProfile: () => mockUseUpdateUserProfile(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('SolutionNavUserProfileToggle', () => {
|
||||
it('renders correctly and toggles opt out of new nav', () => {
|
||||
const security = securityMock.createStart();
|
||||
const core = coreMock.createStart();
|
||||
|
||||
const mockUpdate = jest.fn();
|
||||
mockUseUpdateUserProfile.mockReturnValue({
|
||||
userProfileData: { userSettings: { solutionNavOptOut: undefined } },
|
||||
isLoading: false,
|
||||
update: mockUpdate,
|
||||
userProfileEnabled: true,
|
||||
});
|
||||
|
||||
const { getByTestId, rerender } = render(
|
||||
<SolutionNavUserProfileToggle core={core} security={security} defaultOptOutValue={false} />
|
||||
);
|
||||
|
||||
const toggleSwitch = getByTestId('solutionNavToggleSwitch');
|
||||
fireEvent.click(toggleSwitch);
|
||||
expect(mockUpdate).toHaveBeenCalledWith({ userSettings: { solutionNavOptOut: true } });
|
||||
|
||||
// Now we want to simulate toggling back to light
|
||||
mockUseUpdateUserProfile.mockReturnValue({
|
||||
userProfileData: { userSettings: { solutionNavOptOut: true } },
|
||||
isLoading: false,
|
||||
update: mockUpdate,
|
||||
userProfileEnabled: true,
|
||||
});
|
||||
|
||||
// Rerender the component to apply the new props
|
||||
rerender(
|
||||
<SolutionNavUserProfileToggle core={core} security={security} defaultOptOutValue={false} />
|
||||
);
|
||||
|
||||
fireEvent.click(toggleSwitch);
|
||||
expect(mockUpdate).toHaveBeenLastCalledWith({ userSettings: { solutionNavOptOut: false } });
|
||||
});
|
||||
|
||||
it('does not render if user profile is disabled', async () => {
|
||||
const security = securityMock.createStart();
|
||||
security.userProfiles.enabled$ = of(false);
|
||||
const core = coreMock.createStart();
|
||||
|
||||
const mockUpdate = jest.fn();
|
||||
mockUseUpdateUserProfile.mockReturnValue({
|
||||
userProfileData: { userSettings: { solutionNavOptOut: undefined } },
|
||||
isLoading: false,
|
||||
update: mockUpdate,
|
||||
userProfileEnabled: false,
|
||||
});
|
||||
|
||||
const { queryByTestId } = render(
|
||||
<SolutionNavUserProfileToggle core={core} security={security} defaultOptOutValue={false} />
|
||||
);
|
||||
const toggleSwitch = await queryByTestId('solutionNavToggleSwitch');
|
||||
|
||||
expect(toggleSwitch).toBeNull();
|
||||
});
|
||||
});
|
|
@ -1,91 +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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
EuiContextMenuItem,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSwitch,
|
||||
useEuiTheme,
|
||||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { SecurityPluginStart } from '@kbn/security-plugin/public';
|
||||
import { UserProfilesKibanaProvider } from '@kbn/user-profile-components';
|
||||
import { CoreStart } from '@kbn/core-lifecycle-browser';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
|
||||
import { useSolutionNavUserProfileToggle } from './use_solution_nav_userprofile_toggle';
|
||||
|
||||
interface Props {
|
||||
security: SecurityPluginStart;
|
||||
core: CoreStart;
|
||||
defaultOptOutValue: boolean;
|
||||
}
|
||||
|
||||
export const SolutionNavUserProfileToggle = ({ security, core, defaultOptOutValue }: Props) => {
|
||||
return (
|
||||
<UserProfilesKibanaProvider core={core} security={security} toMountPoint={toMountPoint}>
|
||||
<SolutionNavUserProfileToggleUi defaultOptOutValue={defaultOptOutValue} />
|
||||
</UserProfilesKibanaProvider>
|
||||
);
|
||||
};
|
||||
|
||||
function SolutionNavUserProfileToggleUi({ defaultOptOutValue }: { defaultOptOutValue: boolean }) {
|
||||
const toggleTextSwitchId = useGeneratedHtmlId({ prefix: 'toggleSolutionNavSwitch' });
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const { userProfileEnabled, toggle, hasOptOut } = useSolutionNavUserProfileToggle({
|
||||
defaultOptOutValue,
|
||||
});
|
||||
|
||||
if (!userProfileEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="xs">
|
||||
<EuiFlexItem>
|
||||
<EuiContextMenuItem
|
||||
icon="tableOfContents"
|
||||
size="s"
|
||||
onClick={() => {
|
||||
toggle(!hasOptOut);
|
||||
}}
|
||||
data-test-subj="solutionNavToggle"
|
||||
>
|
||||
{i18n.translate('navigation.userMenuLinks.useClassicNavigation', {
|
||||
defaultMessage: 'Use classic navigation',
|
||||
})}
|
||||
</EuiContextMenuItem>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} css={{ paddingRight: euiTheme.size.m }}>
|
||||
<EuiSwitch
|
||||
label={
|
||||
hasOptOut
|
||||
? i18n.translate('navigation.userMenuLinks.classicNavigationOnLabel', {
|
||||
defaultMessage: 'on',
|
||||
})
|
||||
: i18n.translate('navigation.userMenuLinks.classicNavigationOffLabel', {
|
||||
defaultMessage: 'off',
|
||||
})
|
||||
}
|
||||
showLabel={false}
|
||||
checked={hasOptOut}
|
||||
onChange={(e) => {
|
||||
toggle(e.target.checked);
|
||||
}}
|
||||
aria-describedby={toggleTextSwitchId}
|
||||
data-test-subj="solutionNavToggleSwitch"
|
||||
compressed
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -1,49 +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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useUpdateUserProfile } from '@kbn/user-profile-components';
|
||||
|
||||
interface Deps {
|
||||
defaultOptOutValue: boolean;
|
||||
}
|
||||
|
||||
export const useSolutionNavUserProfileToggle = ({ defaultOptOutValue }: Deps) => {
|
||||
const [hasOptOut, setHasOptOut] = useState(defaultOptOutValue);
|
||||
const { userProfileData, isLoading, update, userProfileEnabled } = useUpdateUserProfile();
|
||||
|
||||
const { userSettings: { solutionNavOptOut = defaultOptOutValue } = {} } = userProfileData ?? {};
|
||||
|
||||
const toggle = useCallback(
|
||||
(on: boolean) => {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
// optimistic update
|
||||
setHasOptOut(on);
|
||||
|
||||
update({
|
||||
userSettings: {
|
||||
solutionNavOptOut: on,
|
||||
},
|
||||
});
|
||||
},
|
||||
[isLoading, update]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setHasOptOut(solutionNavOptOut);
|
||||
}, [solutionNavOptOut]);
|
||||
|
||||
return {
|
||||
toggle,
|
||||
hasOptOut,
|
||||
userProfileEnabled,
|
||||
};
|
||||
};
|
|
@ -11,7 +11,6 @@ import type { Observable } from 'rxjs';
|
|||
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||
import type { SolutionNavigationDefinition } from '@kbn/core-chrome-browser';
|
||||
import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public';
|
||||
import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/public';
|
||||
import type { CloudExperimentsPluginStart } from '@kbn/cloud-experiments-plugin/common';
|
||||
|
||||
import { PanelContentProvider } from '@kbn/shared-ux-chrome-navigation';
|
||||
|
@ -47,21 +46,12 @@ export interface NavigationPublicStart {
|
|||
|
||||
export interface NavigationPublicSetupDependencies {
|
||||
cloud?: CloudSetup;
|
||||
security?: SecurityPluginSetup;
|
||||
}
|
||||
|
||||
export interface NavigationPublicStartDependencies {
|
||||
unifiedSearch: UnifiedSearchPublicPluginStart;
|
||||
cloud?: CloudStart;
|
||||
security?: SecurityPluginStart;
|
||||
cloudExperiments?: CloudExperimentsPluginStart;
|
||||
}
|
||||
|
||||
export type SolutionType = 'es' | 'oblt' | 'security' | 'analytics';
|
||||
|
||||
export interface ConfigSchema {
|
||||
solutionNavigation: {
|
||||
enabled: boolean;
|
||||
defaultSolution: SolutionType;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,34 +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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import type { PluginConfigDescriptor } from '@kbn/core-plugins-server';
|
||||
|
||||
const configSchema = schema.object({
|
||||
solutionNavigation: schema.object({
|
||||
enabled: schema.boolean({ defaultValue: false }),
|
||||
defaultSolution: schema.oneOf(
|
||||
[
|
||||
schema.literal('es'),
|
||||
schema.literal('oblt'),
|
||||
schema.literal('security'),
|
||||
schema.literal('analytics'),
|
||||
],
|
||||
{ defaultValue: 'es' }
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
export type NavigationConfig = TypeOf<typeof configSchema>;
|
||||
|
||||
export const config: PluginConfigDescriptor<NavigationConfig> = {
|
||||
exposeToBrowser: {
|
||||
solutionNavigation: true,
|
||||
},
|
||||
schema: configSchema,
|
||||
};
|
|
@ -5,12 +5,9 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import type { PluginInitializerContext } from '@kbn/core/server';
|
||||
|
||||
import { NavigationServerPlugin } from './plugin';
|
||||
|
||||
export { config } from './config';
|
||||
|
||||
export async function plugin(initializerContext: PluginInitializerContext) {
|
||||
return new NavigationServerPlugin(initializerContext);
|
||||
export async function plugin() {
|
||||
return new NavigationServerPlugin();
|
||||
}
|
||||
|
|
|
@ -5,18 +5,14 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/server';
|
||||
import type { UiSettingsParams } from '@kbn/core/types';
|
||||
import { SOLUTION_NAV_FEATURE_FLAG_NAME } from '../common';
|
||||
import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/server';
|
||||
|
||||
import type { NavigationConfig } from './config';
|
||||
import type {
|
||||
NavigationServerSetup,
|
||||
NavigationServerSetupDependencies,
|
||||
NavigationServerStart,
|
||||
NavigationServerStartDependencies,
|
||||
} from './types';
|
||||
import { getUiSettings } from './ui_settings';
|
||||
|
||||
export class NavigationServerPlugin
|
||||
implements
|
||||
|
@ -27,56 +23,16 @@ export class NavigationServerPlugin
|
|||
NavigationServerStartDependencies
|
||||
>
|
||||
{
|
||||
constructor(private initializerContext: PluginInitializerContext) {}
|
||||
constructor() {}
|
||||
|
||||
setup(
|
||||
core: CoreSetup<NavigationServerStartDependencies>,
|
||||
plugins: NavigationServerSetupDependencies
|
||||
) {
|
||||
const config = this.initializerContext.config.get<NavigationConfig>();
|
||||
const isSolutionNavExperiementEnabled =
|
||||
Boolean(plugins.cloud?.isCloudEnabled) && !this.isServerless();
|
||||
|
||||
if (isSolutionNavExperiementEnabled) {
|
||||
void core.getStartServices().then(([coreStart, deps]) => {
|
||||
return deps.cloudExperiments
|
||||
?.getVariation(SOLUTION_NAV_FEATURE_FLAG_NAME, false)
|
||||
.then(async (enabled) => {
|
||||
if (enabled) {
|
||||
core.uiSettings.registerGlobal(getUiSettings(config));
|
||||
} else {
|
||||
await this.removeUiSettings(coreStart, getUiSettings(config));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
start(core: CoreStart, plugins: NavigationServerStartDependencies) {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove UI settings values that might have been set when the feature was enabled.
|
||||
* If the feature is disabled in kibana.yml, we want to remove the settings from the
|
||||
* saved objects.
|
||||
*
|
||||
* @param core CoreStart
|
||||
* @param uiSettings Navigation UI settings
|
||||
*/
|
||||
private removeUiSettings(core: CoreStart, uiSettings: Record<string, UiSettingsParams>) {
|
||||
if (this.isServerless()) return;
|
||||
|
||||
const savedObjectsClient = core.savedObjects.createInternalRepository();
|
||||
const uiSettingsClient = core.uiSettings.globalAsScopedToClient(savedObjectsClient);
|
||||
|
||||
const keys = Object.keys(uiSettings);
|
||||
return uiSettingsClient.removeMany(keys, { validateKeys: false, handleWriteErrors: true });
|
||||
}
|
||||
|
||||
private isServerless() {
|
||||
return this.initializerContext.env.packageInfo.buildFlavor === 'serverless';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,38 +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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { UiSettingsParams } from '@kbn/core/types';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { ENABLE_SOLUTION_NAV_UI_SETTING_ID } from '../common/constants';
|
||||
import { NavigationConfig } from './config';
|
||||
|
||||
const categoryLabel = i18n.translate('navigation.uiSettings.categoryLabel', {
|
||||
defaultMessage: 'Technical preview',
|
||||
});
|
||||
|
||||
/**
|
||||
* uiSettings definitions for Navigation
|
||||
*/
|
||||
export const getUiSettings = (config: NavigationConfig): Record<string, UiSettingsParams> => {
|
||||
return {
|
||||
[ENABLE_SOLUTION_NAV_UI_SETTING_ID]: {
|
||||
category: [categoryLabel],
|
||||
name: i18n.translate('navigation.uiSettings.enableSolutionNav.name', {
|
||||
defaultMessage: 'Enable solution navigation',
|
||||
}),
|
||||
description: i18n.translate('navigation.uiSettings.enableSolutionNav.description', {
|
||||
defaultMessage: 'Let users opt in or out from the new solution navigation experience.',
|
||||
}),
|
||||
schema: schema.boolean(),
|
||||
value: config.solutionNavigation.enabled,
|
||||
order: 1,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -21,13 +21,8 @@
|
|||
"@kbn/core-chrome-browser-internal",
|
||||
"@kbn/shared-ux-chrome-navigation",
|
||||
"@kbn/cloud-plugin",
|
||||
"@kbn/config-schema",
|
||||
"@kbn/core-plugins-server",
|
||||
"@kbn/i18n",
|
||||
"@kbn/config",
|
||||
"@kbn/security-plugin",
|
||||
"@kbn/user-profile-components",
|
||||
"@kbn/core-lifecycle-browser",
|
||||
"@kbn/cloud-experiments-plugin",
|
||||
],
|
||||
"exclude": [
|
||||
|
|
|
@ -161,8 +161,6 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
|
|||
'monitoring.ui.enabled (boolean)',
|
||||
'monitoring.ui.min_interval_seconds (number)',
|
||||
'monitoring.ui.show_license_expiration (boolean)',
|
||||
'navigation.solutionNavigation.enabled (boolean)',
|
||||
'navigation.solutionNavigation.defaultSolution (alternatives)',
|
||||
'newsfeed.fetchInterval (duration)',
|
||||
'newsfeed.mainInterval (duration)',
|
||||
'newsfeed.service.pathTemplate (string)',
|
||||
|
|
|
@ -1,55 +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 { FtrConfigProviderContext } from '@kbn/test';
|
||||
import { services, pageObjects } from './ftr_provider_context';
|
||||
|
||||
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
||||
const kibanaFunctionalConfig = await readConfigFile(require.resolve('../../config.base.js'));
|
||||
|
||||
return {
|
||||
testFiles: [require.resolve('./tests')],
|
||||
servers: {
|
||||
...kibanaFunctionalConfig.get('servers'),
|
||||
},
|
||||
services,
|
||||
pageObjects,
|
||||
|
||||
junit: {
|
||||
reportName: 'X-Pack Navigation Functional Tests',
|
||||
},
|
||||
|
||||
esTestCluster: {
|
||||
...kibanaFunctionalConfig.get('esTestCluster'),
|
||||
license: 'trial',
|
||||
serverArgs: [`xpack.license.self_generated.type='trial'`],
|
||||
},
|
||||
apps: {
|
||||
...kibanaFunctionalConfig.get('apps'),
|
||||
},
|
||||
|
||||
kbnTestServer: {
|
||||
...kibanaFunctionalConfig.get('kbnTestServer'),
|
||||
serverArgs: [
|
||||
...kibanaFunctionalConfig.get('kbnTestServer.serverArgs'),
|
||||
'--xpack.cloud_integrations.experiments.enabled=true',
|
||||
'--xpack.cloud_integrations.experiments.launch_darkly.sdk_key=a_string',
|
||||
'--xpack.cloud_integrations.experiments.launch_darkly.client_id=a_string',
|
||||
'--xpack.cloud_integrations.experiments.flag_overrides.solutionNavEnabled=true',
|
||||
'--navigation.solutionNavigation.enabled=true',
|
||||
'--navigation.solutionNavigation.defaultSolution=es',
|
||||
// Note: the base64 string in the cloud.id config contains the ES endpoint required in the functional tests
|
||||
'--xpack.cloud.id=ftr_fake_cloud_id:aGVsbG8uY29tOjQ0MyRFUzEyM2FiYyRrYm4xMjNhYmM=',
|
||||
'--xpack.cloud.base_url=https://cloud.elastic.co',
|
||||
'--xpack.cloud.deployment_url=/deployments/deploymentId',
|
||||
'--xpack.cloud.organization_url=/organization/organizationId',
|
||||
'--xpack.cloud.billing_url=/billing',
|
||||
'--xpack.cloud.profile_url=/user/userId',
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,13 +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 { GenericFtrProviderContext } from '@kbn/test';
|
||||
import { services } from '../../services';
|
||||
import { pageObjects } from '../../page_objects';
|
||||
|
||||
export type FtrProviderContext = GenericFtrProviderContext<typeof services, typeof pageObjects>;
|
||||
export { services, pageObjects };
|
|
@ -1,15 +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 { FtrProviderContext } from '../ftr_provider_context';
|
||||
|
||||
export default function ({ loadTestFile }: FtrProviderContext) {
|
||||
describe('navigation - functional tests', function () {
|
||||
loadTestFile(require.resolve('./solution_nav_switcher'));
|
||||
loadTestFile(require.resolve('./user_optin_optout'));
|
||||
});
|
||||
}
|
|
@ -1,38 +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 { FtrProviderContext } from '../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const PageObjects = getPageObjects(['common', 'header', 'discover', 'dashboard']);
|
||||
const testSubjects = getService('testSubjects');
|
||||
const navigation = getService('globalNav');
|
||||
|
||||
describe('solution navigation switcher', function describeIndexTests() {
|
||||
it('should be able to switch between solutions', async () => {
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
|
||||
// Default to "search" solution
|
||||
await testSubjects.existOrFail('searchSideNav');
|
||||
await testSubjects.missingOrFail('observabilitySideNav');
|
||||
|
||||
// Change to "observability" solution
|
||||
await navigation.changeSolutionNavigation('oblt');
|
||||
await testSubjects.existOrFail('observabilitySideNav');
|
||||
await testSubjects.missingOrFail('searchSideNav');
|
||||
});
|
||||
|
||||
it('should contain links to manage deployment and view all deployments', async () => {
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
|
||||
await navigation.openSolutionNavSwitcher();
|
||||
|
||||
await testSubjects.existOrFail('manageDeploymentBtn', { timeout: 2000 });
|
||||
await testSubjects.existOrFail('viewDeploymentsBtn', { timeout: 2000 });
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,36 +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 { FtrProviderContext } from '../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const PageObjects = getPageObjects(['common', 'header', 'home', 'dashboard', 'security']);
|
||||
const testSubjects = getService('testSubjects');
|
||||
const userMenu = getService('userMenu');
|
||||
|
||||
describe('user opt in/out', function describeIndexTests() {
|
||||
it('should allow the user to opt in or out', async () => {
|
||||
await PageObjects.common.navigateToApp('home');
|
||||
|
||||
// we are in the new nav, search solution
|
||||
await testSubjects.existOrFail('kibanaProjectHeader');
|
||||
|
||||
await userMenu.openMenu();
|
||||
await testSubjects.existOrFail('solutionNavToggle');
|
||||
|
||||
// Opt OUT of the new navigation
|
||||
await testSubjects.click('solutionNavToggle');
|
||||
// we are in the old nav
|
||||
await testSubjects.missingOrFail('kibanaProjectHeader');
|
||||
|
||||
// Opt back IN to the new navigation
|
||||
await userMenu.openMenu();
|
||||
await testSubjects.click('solutionNavToggle');
|
||||
await testSubjects.existOrFail('kibanaProjectHeader');
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,32 +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 { FtrConfigProviderContext } from '@kbn/test';
|
||||
|
||||
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
||||
const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js'));
|
||||
|
||||
return {
|
||||
...functionalConfig.getAll(),
|
||||
testFiles: [require.resolve('.')],
|
||||
kbnTestServer: {
|
||||
...functionalConfig.get('kbnTestServer'),
|
||||
serverArgs: [
|
||||
...functionalConfig.get('kbnTestServer.serverArgs'),
|
||||
'--xpack.cloud_integrations.experiments.enabled=true',
|
||||
'--xpack.cloud_integrations.experiments.launch_darkly.sdk_key=a_string',
|
||||
'--xpack.cloud_integrations.experiments.launch_darkly.client_id=a_string',
|
||||
'--xpack.cloud_integrations.experiments.flag_overrides.solutionNavEnabled=true',
|
||||
'--navigation.solutionNavigation.enabled=true',
|
||||
// Note: the base64 string in the cloud.id config contains the ES endpoint required in the functional tests
|
||||
'--xpack.cloud.id=ftr_fake_cloud_id:aGVsbG8uY29tOjQ0MyRFUzEyM2FiYyRrYm4xMjNhYmM=',
|
||||
'--xpack.cloud.base_url=https://cloud.elastic.co',
|
||||
'--xpack.cloud.deployment_url=/deployments/deploymentId',
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,14 +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 { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function spacesApp({ loadTestFile }: FtrProviderContext) {
|
||||
describe('Spaces app - solution navigation', function spacesAppTestSuite() {
|
||||
loadTestFile(require.resolve('./spaces_selection'));
|
||||
});
|
||||
}
|
|
@ -1,108 +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 { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function spaceSelectorFunctionalTests({
|
||||
getService,
|
||||
getPageObjects,
|
||||
}: FtrProviderContext) {
|
||||
const PageObjects = getPageObjects([
|
||||
'common',
|
||||
'dashboard',
|
||||
'header',
|
||||
'home',
|
||||
'security',
|
||||
'spaceSelector',
|
||||
]);
|
||||
const spacesService = getService('spaces');
|
||||
|
||||
// NOTE: Those tests have been copied over from the parent folder "spaces_selection.ts"
|
||||
// We want to test that under the (upcoming) solution navigation, the spaces selector works as expected
|
||||
// Once the solution navigation becomes the default we can remove this "in_solution_navigation" folder
|
||||
// and rely only on the tests in the parent folder.
|
||||
describe('Spaces', function () {
|
||||
const testSpacesIds = ['another-space', ...Array.from('123456789', (idx) => `space-${idx}`)];
|
||||
before(async () => {
|
||||
for (const testSpaceId of testSpacesIds) {
|
||||
await spacesService.create({ id: testSpaceId, name: `${testSpaceId} name` });
|
||||
}
|
||||
});
|
||||
after(async () => {
|
||||
for (const testSpaceId of testSpacesIds) {
|
||||
await spacesService.delete(testSpaceId);
|
||||
}
|
||||
});
|
||||
|
||||
describe('Space Navigation Menu', () => {
|
||||
before(async () => {
|
||||
await PageObjects.security.forceLogout();
|
||||
await PageObjects.security.login(undefined, undefined, {
|
||||
expectSpaceSelector: true,
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await PageObjects.security.forceLogout();
|
||||
});
|
||||
|
||||
it('allows user to navigate to different spaces', async () => {
|
||||
const anotherSpaceId = 'another-space';
|
||||
const defaultSpaceId = 'default';
|
||||
const space5Id = 'space-5';
|
||||
|
||||
await PageObjects.spaceSelector.clickSpaceCard(defaultSpaceId);
|
||||
await PageObjects.spaceSelector.expectHomePage(defaultSpaceId);
|
||||
|
||||
// change spaces with nav menu
|
||||
await PageObjects.spaceSelector.openSpacesNav();
|
||||
await PageObjects.spaceSelector.goToSpecificSpace(space5Id);
|
||||
await PageObjects.spaceSelector.expectHomePage(space5Id);
|
||||
|
||||
await PageObjects.spaceSelector.openSpacesNav();
|
||||
await PageObjects.spaceSelector.goToSpecificSpace(anotherSpaceId);
|
||||
await PageObjects.spaceSelector.expectHomePage(anotherSpaceId);
|
||||
|
||||
await PageObjects.spaceSelector.openSpacesNav();
|
||||
await PageObjects.spaceSelector.goToSpecificSpace(defaultSpaceId);
|
||||
await PageObjects.spaceSelector.expectHomePage(defaultSpaceId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Search spaces in popover', function () {
|
||||
const spaceId = 'default';
|
||||
before(async () => {
|
||||
await PageObjects.security.forceLogout();
|
||||
await PageObjects.security.login(undefined, undefined, {
|
||||
expectSpaceSelector: true,
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await PageObjects.security.forceLogout();
|
||||
});
|
||||
|
||||
it('allows user to search for spaces', async () => {
|
||||
await PageObjects.spaceSelector.clickSpaceCard(spaceId);
|
||||
await PageObjects.spaceSelector.expectHomePage(spaceId);
|
||||
await PageObjects.spaceSelector.openSpacesNav();
|
||||
await PageObjects.spaceSelector.expectSearchBoxInSpacesSelector();
|
||||
});
|
||||
|
||||
it('search for "ce-1 name" and find one space', async () => {
|
||||
await PageObjects.spaceSelector.setSearchBoxInSpacesSelector('ce-1 name');
|
||||
await PageObjects.spaceSelector.expectToFindThatManySpace(1);
|
||||
});
|
||||
|
||||
it('search for "dog" and find NO space', async () => {
|
||||
await PageObjects.spaceSelector.setSearchBoxInSpacesSelector('dog');
|
||||
await PageObjects.spaceSelector.expectToFindThatManySpace(0);
|
||||
await PageObjects.spaceSelector.expectNoSpacesFound();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue