mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 11:05:39 -04:00
[Stateful sidenav] Remove Launch Darkly feature flag (#189513)
This commit is contained in:
parent
0298013a78
commit
03607ec7e0
39 changed files with 425 additions and 467 deletions
|
@ -152,6 +152,7 @@ server.versioned.strictClientVersionCheck: false
|
||||||
# Enforce single "default" space and disable feature visibility controls
|
# Enforce single "default" space and disable feature visibility controls
|
||||||
xpack.spaces.maxSpaces: 1
|
xpack.spaces.maxSpaces: 1
|
||||||
xpack.spaces.allowFeatureVisibility: false
|
xpack.spaces.allowFeatureVisibility: false
|
||||||
|
xpack.spaces.allowSolutionVisibility: false
|
||||||
|
|
||||||
# Only display console autocomplete suggestions for ES endpoints that are available in serverless
|
# Only display console autocomplete suggestions for ES endpoints that are available in serverless
|
||||||
console.autocompleteDefinitions.endpointsAvailability: serverless
|
console.autocompleteDefinitions.endpointsAvailability: serverless
|
||||||
|
|
|
@ -54,6 +54,7 @@ export type Result = 'ready';
|
||||||
if (serverless) {
|
if (serverless) {
|
||||||
// Satisfy spaces config for serverless:
|
// Satisfy spaces config for serverless:
|
||||||
set(settings, 'xpack.spaces.allowFeatureVisibility', false);
|
set(settings, 'xpack.spaces.allowFeatureVisibility', false);
|
||||||
|
set(settings, 'xpack.spaces.allowSolutionVisibility', false);
|
||||||
const { startKibana } = createTestServerlessInstances({
|
const { startKibana } = createTestServerlessInstances({
|
||||||
kibana: { settings, cliArgs },
|
kibana: { settings, cliArgs },
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,8 +6,6 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const SOLUTION_NAV_FEATURE_FLAG_NAME = 'solutionNavEnabled';
|
|
||||||
|
|
||||||
export const DEFAULT_ROUTE_UI_SETTING_ID = 'defaultRoute';
|
export const DEFAULT_ROUTE_UI_SETTING_ID = 'defaultRoute';
|
||||||
|
|
||||||
export const DEFAULT_ROUTES = {
|
export const DEFAULT_ROUTES = {
|
||||||
|
|
|
@ -6,4 +6,4 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { SOLUTION_NAV_FEATURE_FLAG_NAME } from './constants';
|
export { DEFAULT_ROUTES, DEFAULT_ROUTE_UI_SETTING_ID } from './constants';
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
"id": "navigation",
|
"id": "navigation",
|
||||||
"server": true,
|
"server": true,
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"optionalPlugins": ["cloud", "cloudExperiments", "spaces"],
|
"optionalPlugins": ["cloud", "spaces"],
|
||||||
"requiredPlugins": ["unifiedSearch"],
|
"requiredPlugins": ["unifiedSearch"],
|
||||||
"requiredBundles": []
|
"requiredBundles": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,11 +10,9 @@ import { firstValueFrom, of } from 'rxjs';
|
||||||
import { coreMock } from '@kbn/core/public/mocks';
|
import { coreMock } from '@kbn/core/public/mocks';
|
||||||
import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks';
|
import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks';
|
||||||
import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
|
import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
|
||||||
import { cloudExperimentsMock } from '@kbn/cloud-experiments-plugin/common/mocks';
|
|
||||||
import { spacesPluginMock } from '@kbn/spaces-plugin/public/mocks';
|
import { spacesPluginMock } from '@kbn/spaces-plugin/public/mocks';
|
||||||
import type { Space } from '@kbn/spaces-plugin/public';
|
import type { Space } from '@kbn/spaces-plugin/public';
|
||||||
import type { BuildFlavor } from '@kbn/config';
|
import type { BuildFlavor } from '@kbn/config';
|
||||||
import { SOLUTION_NAV_FEATURE_FLAG_NAME } from '../common';
|
|
||||||
import { NavigationPublicPlugin } from './plugin';
|
import { NavigationPublicPlugin } from './plugin';
|
||||||
|
|
||||||
jest.mock('rxjs', () => {
|
jest.mock('rxjs', () => {
|
||||||
|
@ -25,16 +23,11 @@ jest.mock('rxjs', () => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const setup = (
|
const setup = ({
|
||||||
config: {
|
|
||||||
featureOn: boolean;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
buildFlavor = 'traditional',
|
buildFlavor = 'traditional',
|
||||||
}: {
|
}: {
|
||||||
buildFlavor?: BuildFlavor;
|
buildFlavor?: BuildFlavor;
|
||||||
} = {}
|
} = {}) => {
|
||||||
) => {
|
|
||||||
const initializerContext = coreMock.createPluginInitializerContext({}, { buildFlavor });
|
const initializerContext = coreMock.createPluginInitializerContext({}, { buildFlavor });
|
||||||
const plugin = new NavigationPublicPlugin(initializerContext);
|
const plugin = new NavigationPublicPlugin(initializerContext);
|
||||||
|
|
||||||
|
@ -42,14 +35,7 @@ const setup = (
|
||||||
const coreStart = coreMock.createStart();
|
const coreStart = coreMock.createStart();
|
||||||
const unifiedSearch = unifiedSearchPluginMock.createStartContract();
|
const unifiedSearch = unifiedSearchPluginMock.createStartContract();
|
||||||
const cloud = cloudMock.createStart();
|
const cloud = cloudMock.createStart();
|
||||||
const cloudExperiments = cloudExperimentsMock.createStartMock();
|
|
||||||
const spaces = spacesPluginMock.createStartContract();
|
const spaces = spacesPluginMock.createStartContract();
|
||||||
cloudExperiments.getVariation.mockImplementation((key) => {
|
|
||||||
if (key === SOLUTION_NAV_FEATURE_FLAG_NAME) {
|
|
||||||
return Promise.resolve(config.featureOn);
|
|
||||||
}
|
|
||||||
return Promise.resolve(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
const getGlobalSetting$ = jest.fn();
|
const getGlobalSetting$ = jest.fn();
|
||||||
const settingsGlobalClient = {
|
const settingsGlobalClient = {
|
||||||
|
@ -64,46 +50,20 @@ const setup = (
|
||||||
coreStart,
|
coreStart,
|
||||||
unifiedSearch,
|
unifiedSearch,
|
||||||
cloud,
|
cloud,
|
||||||
cloudExperiments,
|
|
||||||
spaces,
|
spaces,
|
||||||
config,
|
|
||||||
setChromeStyle,
|
setChromeStyle,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Navigation Plugin', () => {
|
describe('Navigation Plugin', () => {
|
||||||
describe('feature flag disabled', () => {
|
|
||||||
const featureOn = false;
|
|
||||||
|
|
||||||
it('should not add the default solutions nor set the active nav if the feature is disabled', () => {
|
|
||||||
const { plugin, coreStart, unifiedSearch } = setup({ featureOn });
|
|
||||||
plugin.start(coreStart, { unifiedSearch });
|
|
||||||
expect(coreStart.chrome.project.updateSolutionNavigations).not.toHaveBeenCalled();
|
|
||||||
expect(coreStart.chrome.project.changeActiveSolutionNavigation).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return flag to indicate that the solution navigation is disabled', async () => {
|
|
||||||
const { plugin, coreStart, unifiedSearch } = setup({ featureOn });
|
|
||||||
const isEnabled = await firstValueFrom(
|
|
||||||
plugin.start(coreStart, { unifiedSearch }).isSolutionNavEnabled$
|
|
||||||
);
|
|
||||||
expect(isEnabled).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('feature flag enabled', () => {
|
|
||||||
const featureOn = true;
|
|
||||||
|
|
||||||
it('should change the active solution navigation', async () => {
|
it('should change the active solution navigation', async () => {
|
||||||
const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments, spaces } = setup({
|
const { plugin, coreStart, unifiedSearch, cloud, spaces } = setup();
|
||||||
featureOn,
|
|
||||||
});
|
|
||||||
|
|
||||||
spaces.getActiveSpace$ = jest
|
spaces.getActiveSpace$ = jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockReturnValue(of({ solution: 'es' } as Pick<Space, 'solution'>));
|
.mockReturnValue(of({ solution: 'es' } as Pick<Space, 'solution'>));
|
||||||
|
|
||||||
plugin.start(coreStart, { unifiedSearch, cloud, cloudExperiments, spaces });
|
plugin.start(coreStart, { unifiedSearch, cloud, spaces });
|
||||||
await new Promise((resolve) => setTimeout(resolve));
|
await new Promise((resolve) => setTimeout(resolve));
|
||||||
|
|
||||||
expect(coreStart.chrome.project.changeActiveSolutionNavigation).toHaveBeenCalledWith('es');
|
expect(coreStart.chrome.project.changeActiveSolutionNavigation).toHaveBeenCalledWith('es');
|
||||||
|
@ -111,14 +71,11 @@ describe('Navigation Plugin', () => {
|
||||||
|
|
||||||
describe('addSolutionNavigation()', () => {
|
describe('addSolutionNavigation()', () => {
|
||||||
it('should update the solution navigation definitions', async () => {
|
it('should update the solution navigation definitions', async () => {
|
||||||
const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments } = setup({
|
const { plugin, coreStart, unifiedSearch, cloud } = setup();
|
||||||
featureOn,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { addSolutionNavigation } = plugin.start(coreStart, {
|
const { addSolutionNavigation } = plugin.start(coreStart, {
|
||||||
unifiedSearch,
|
unifiedSearch,
|
||||||
cloud,
|
cloud,
|
||||||
cloudExperiments,
|
|
||||||
});
|
});
|
||||||
await new Promise((resolve) => setTimeout(resolve));
|
await new Promise((resolve) => setTimeout(resolve));
|
||||||
|
|
||||||
|
@ -141,34 +98,20 @@ describe('Navigation Plugin', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('set Chrome style', () => {
|
describe('set Chrome style', () => {
|
||||||
it('should set the Chrome style to "classic" when the feature is not enabled', async () => {
|
|
||||||
const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments } = setup(
|
|
||||||
{ featureOn: false } // feature not enabled
|
|
||||||
);
|
|
||||||
|
|
||||||
plugin.start(coreStart, { unifiedSearch, cloud, cloudExperiments });
|
|
||||||
await new Promise((resolve) => setTimeout(resolve));
|
|
||||||
expect(coreStart.chrome.setChromeStyle).toHaveBeenCalledWith('classic');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set the Chrome style to "classic" when spaces plugin is not available', async () => {
|
it('should set the Chrome style to "classic" when spaces plugin is not available', async () => {
|
||||||
const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments } = setup(
|
const { plugin, coreStart, unifiedSearch, cloud } = setup();
|
||||||
{ featureOn: true } // feature not enabled but no spaces plugin
|
|
||||||
);
|
|
||||||
|
|
||||||
plugin.start(coreStart, { unifiedSearch, cloud, cloudExperiments });
|
plugin.start(coreStart, { unifiedSearch, cloud });
|
||||||
await new Promise((resolve) => setTimeout(resolve));
|
await new Promise((resolve) => setTimeout(resolve));
|
||||||
expect(coreStart.chrome.setChromeStyle).toHaveBeenCalledWith('classic');
|
expect(coreStart.chrome.setChromeStyle).toHaveBeenCalledWith('classic');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the Chrome style to "classic" when active space solution is "classic"', async () => {
|
it('should set the Chrome style to "classic" when active space solution is "classic"', async () => {
|
||||||
const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments, spaces } = setup({
|
const { plugin, coreStart, unifiedSearch, cloud, spaces } = setup();
|
||||||
featureOn: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Spaces plugin is available but activeSpace is undefined
|
// Spaces plugin is available but activeSpace is undefined
|
||||||
spaces.getActiveSpace$ = jest.fn().mockReturnValue(of(undefined));
|
spaces.getActiveSpace$ = jest.fn().mockReturnValue(of(undefined));
|
||||||
plugin.start(coreStart, { unifiedSearch, cloud, cloudExperiments, spaces });
|
plugin.start(coreStart, { unifiedSearch, cloud, spaces });
|
||||||
await new Promise((resolve) => setTimeout(resolve));
|
await new Promise((resolve) => setTimeout(resolve));
|
||||||
expect(coreStart.chrome.setChromeStyle).toHaveBeenCalledWith('classic');
|
expect(coreStart.chrome.setChromeStyle).toHaveBeenCalledWith('classic');
|
||||||
|
|
||||||
|
@ -177,39 +120,34 @@ describe('Navigation Plugin', () => {
|
||||||
spaces.getActiveSpace$ = jest
|
spaces.getActiveSpace$ = jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockReturnValue(of({ solution: 'classic' } as Pick<Space, 'solution'>));
|
.mockReturnValue(of({ solution: 'classic' } as Pick<Space, 'solution'>));
|
||||||
plugin.start(coreStart, { unifiedSearch, cloud, cloudExperiments, spaces });
|
plugin.start(coreStart, { unifiedSearch, cloud, spaces });
|
||||||
await new Promise((resolve) => setTimeout(resolve));
|
await new Promise((resolve) => setTimeout(resolve));
|
||||||
expect(coreStart.chrome.setChromeStyle).toHaveBeenCalledWith('classic');
|
expect(coreStart.chrome.setChromeStyle).toHaveBeenCalledWith('classic');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should NOT set the Chrome style when the feature is enabled BUT on serverless', async () => {
|
it('should NOT set the Chrome style when on serverless', async () => {
|
||||||
const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments } = setup(
|
const { plugin, coreStart, unifiedSearch, cloud } = setup({ buildFlavor: 'serverless' });
|
||||||
{ featureOn: true }, // feature enabled
|
|
||||||
{ buildFlavor: 'serverless' }
|
|
||||||
);
|
|
||||||
|
|
||||||
plugin.start(coreStart, { unifiedSearch, cloud, cloudExperiments });
|
plugin.start(coreStart, { unifiedSearch, cloud });
|
||||||
await new Promise((resolve) => setTimeout(resolve));
|
await new Promise((resolve) => setTimeout(resolve));
|
||||||
expect(coreStart.chrome.setChromeStyle).not.toHaveBeenCalled();
|
expect(coreStart.chrome.setChromeStyle).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the Chrome style to "project" when space solution is a known solution', async () => {
|
it('should set the Chrome style to "project" when space solution is a known solution', async () => {
|
||||||
const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments, spaces } = setup({
|
const { plugin, coreStart, unifiedSearch, cloud, spaces } = setup();
|
||||||
featureOn: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const solution of ['es', 'oblt', 'security']) {
|
for (const solution of ['es', 'oblt', 'security']) {
|
||||||
spaces.getActiveSpace$ = jest
|
spaces.getActiveSpace$ = jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockReturnValue(of({ solution } as Pick<Space, 'solution'>));
|
.mockReturnValue(of({ solution } as Pick<Space, 'solution'>));
|
||||||
plugin.start(coreStart, { unifiedSearch, cloud, cloudExperiments, spaces });
|
plugin.start(coreStart, { unifiedSearch, cloud, spaces });
|
||||||
await new Promise((resolve) => setTimeout(resolve));
|
await new Promise((resolve) => setTimeout(resolve));
|
||||||
expect(coreStart.chrome.setChromeStyle).toHaveBeenCalledWith('project');
|
expect(coreStart.chrome.setChromeStyle).toHaveBeenCalledWith('project');
|
||||||
coreStart.chrome.setChromeStyle.mockReset();
|
coreStart.chrome.setChromeStyle.mockReset();
|
||||||
}
|
}
|
||||||
|
|
||||||
spaces.getActiveSpace$ = jest.fn().mockReturnValue(of({ solution: 'unknown' }));
|
spaces.getActiveSpace$ = jest.fn().mockReturnValue(of({ solution: 'unknown' }));
|
||||||
plugin.start(coreStart, { unifiedSearch, cloud, cloudExperiments, spaces });
|
plugin.start(coreStart, { unifiedSearch, cloud, spaces });
|
||||||
await new Promise((resolve) => setTimeout(resolve));
|
await new Promise((resolve) => setTimeout(resolve));
|
||||||
expect(coreStart.chrome.setChromeStyle).toHaveBeenCalledWith('classic');
|
expect(coreStart.chrome.setChromeStyle).toHaveBeenCalledWith('classic');
|
||||||
});
|
});
|
||||||
|
@ -218,7 +156,7 @@ describe('Navigation Plugin', () => {
|
||||||
describe('isSolutionNavEnabled$', () => {
|
describe('isSolutionNavEnabled$', () => {
|
||||||
// This test will need to be changed when we remove the feature flag
|
// This test will need to be changed when we remove the feature flag
|
||||||
it('should be off by default', async () => {
|
it('should be off by default', async () => {
|
||||||
const { plugin, coreStart, unifiedSearch, cloud } = setup({ featureOn });
|
const { plugin, coreStart, unifiedSearch, cloud } = setup();
|
||||||
|
|
||||||
const { isSolutionNavEnabled$ } = plugin.start(coreStart, {
|
const { isSolutionNavEnabled$ } = plugin.start(coreStart, {
|
||||||
unifiedSearch,
|
unifiedSearch,
|
||||||
|
@ -230,12 +168,8 @@ describe('Navigation Plugin', () => {
|
||||||
expect(isEnabled).toBe(false);
|
expect(isEnabled).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be off if feature flag if "ON" but space solution is "classic" or "undefined"', async () => {
|
it('should be off if space solution is "classic" or "undefined"', async () => {
|
||||||
const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments, spaces } = setup({
|
const { plugin, coreStart, unifiedSearch, cloud, spaces } = setup();
|
||||||
featureOn,
|
|
||||||
});
|
|
||||||
|
|
||||||
cloudExperiments.getVariation.mockResolvedValue(true); // Feature flag ON
|
|
||||||
|
|
||||||
{
|
{
|
||||||
spaces.getActiveSpace$ = jest
|
spaces.getActiveSpace$ = jest
|
||||||
|
@ -245,7 +179,6 @@ describe('Navigation Plugin', () => {
|
||||||
const { isSolutionNavEnabled$ } = plugin.start(coreStart, {
|
const { isSolutionNavEnabled$ } = plugin.start(coreStart, {
|
||||||
unifiedSearch,
|
unifiedSearch,
|
||||||
cloud,
|
cloud,
|
||||||
cloudExperiments,
|
|
||||||
spaces,
|
spaces,
|
||||||
});
|
});
|
||||||
await new Promise((resolve) => setTimeout(resolve));
|
await new Promise((resolve) => setTimeout(resolve));
|
||||||
|
@ -262,7 +195,6 @@ describe('Navigation Plugin', () => {
|
||||||
const { isSolutionNavEnabled$ } = plugin.start(coreStart, {
|
const { isSolutionNavEnabled$ } = plugin.start(coreStart, {
|
||||||
unifiedSearch,
|
unifiedSearch,
|
||||||
cloud,
|
cloud,
|
||||||
cloudExperiments,
|
|
||||||
spaces,
|
spaces,
|
||||||
});
|
});
|
||||||
await new Promise((resolve) => setTimeout(resolve));
|
await new Promise((resolve) => setTimeout(resolve));
|
||||||
|
@ -272,12 +204,8 @@ describe('Navigation Plugin', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be on if feature flag if "ON" and space solution is set', async () => {
|
it('should be on if space solution is set', async () => {
|
||||||
const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments, spaces } = setup({
|
const { plugin, coreStart, unifiedSearch, cloud, spaces } = setup();
|
||||||
featureOn,
|
|
||||||
});
|
|
||||||
|
|
||||||
cloudExperiments.getVariation.mockResolvedValue(true); // Feature flag ON
|
|
||||||
|
|
||||||
spaces.getActiveSpace$ = jest
|
spaces.getActiveSpace$ = jest
|
||||||
.fn()
|
.fn()
|
||||||
|
@ -286,7 +214,6 @@ describe('Navigation Plugin', () => {
|
||||||
const { isSolutionNavEnabled$ } = plugin.start(coreStart, {
|
const { isSolutionNavEnabled$ } = plugin.start(coreStart, {
|
||||||
unifiedSearch,
|
unifiedSearch,
|
||||||
cloud,
|
cloud,
|
||||||
cloudExperiments,
|
|
||||||
spaces,
|
spaces,
|
||||||
});
|
});
|
||||||
await new Promise((resolve) => setTimeout(resolve));
|
await new Promise((resolve) => setTimeout(resolve));
|
||||||
|
@ -295,16 +222,12 @@ describe('Navigation Plugin', () => {
|
||||||
expect(isEnabled).toBe(true);
|
expect(isEnabled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('on serverless should flag must be disabled', async () => {
|
it('on serverless flag must be disabled', async () => {
|
||||||
const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments } = setup(
|
const { plugin, coreStart, unifiedSearch, cloud } = setup({ buildFlavor: 'serverless' });
|
||||||
{ featureOn },
|
|
||||||
{ buildFlavor: 'serverless' }
|
|
||||||
);
|
|
||||||
|
|
||||||
const { isSolutionNavEnabled$ } = plugin.start(coreStart, {
|
const { isSolutionNavEnabled$ } = plugin.start(coreStart, {
|
||||||
unifiedSearch,
|
unifiedSearch,
|
||||||
cloud,
|
cloud,
|
||||||
cloudExperiments,
|
|
||||||
});
|
});
|
||||||
await new Promise((resolve) => setTimeout(resolve));
|
await new Promise((resolve) => setTimeout(resolve));
|
||||||
|
|
||||||
|
@ -312,5 +235,4 @@ describe('Navigation Plugin', () => {
|
||||||
expect(isEnabled).toBe(false);
|
expect(isEnabled).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,23 +6,13 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import { of, ReplaySubject, take, map, Observable } from 'rxjs';
|
||||||
firstValueFrom,
|
|
||||||
from,
|
|
||||||
of,
|
|
||||||
ReplaySubject,
|
|
||||||
shareReplay,
|
|
||||||
take,
|
|
||||||
combineLatest,
|
|
||||||
map,
|
|
||||||
} from 'rxjs';
|
|
||||||
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
|
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
|
||||||
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||||
import type { Space } from '@kbn/spaces-plugin/public';
|
import type { Space } from '@kbn/spaces-plugin/public';
|
||||||
import type { SolutionNavigationDefinition } from '@kbn/core-chrome-browser';
|
import type { SolutionNavigationDefinition } from '@kbn/core-chrome-browser';
|
||||||
import { InternalChromeStart } from '@kbn/core-chrome-browser-internal';
|
import { InternalChromeStart } from '@kbn/core-chrome-browser-internal';
|
||||||
import type { PanelContentProvider } from '@kbn/shared-ux-chrome-navigation';
|
import type { PanelContentProvider } from '@kbn/shared-ux-chrome-navigation';
|
||||||
import { SOLUTION_NAV_FEATURE_FLAG_NAME } from '../common';
|
|
||||||
import type {
|
import type {
|
||||||
NavigationPublicSetup,
|
NavigationPublicSetup,
|
||||||
NavigationPublicStart,
|
NavigationPublicStart,
|
||||||
|
@ -49,7 +39,7 @@ export class NavigationPublicPlugin
|
||||||
private readonly stop$ = new ReplaySubject<void>(1);
|
private readonly stop$ = new ReplaySubject<void>(1);
|
||||||
private coreStart?: CoreStart;
|
private coreStart?: CoreStart;
|
||||||
private depsStart?: NavigationPublicStartDependencies;
|
private depsStart?: NavigationPublicStartDependencies;
|
||||||
private isSolutionNavExperiementEnabled$ = of(false);
|
private isSolutionNavEnabled = false;
|
||||||
|
|
||||||
constructor(private initializerContext: PluginInitializerContext) {}
|
constructor(private initializerContext: PluginInitializerContext) {}
|
||||||
|
|
||||||
|
@ -70,10 +60,14 @@ export class NavigationPublicPlugin
|
||||||
this.coreStart = core;
|
this.coreStart = core;
|
||||||
this.depsStart = depsStart;
|
this.depsStart = depsStart;
|
||||||
|
|
||||||
const { unifiedSearch, cloud, cloudExperiments, spaces } = depsStart;
|
const { unifiedSearch, cloud, spaces } = depsStart;
|
||||||
const extensions = this.topNavMenuExtensionsRegistry.getAll();
|
const extensions = this.topNavMenuExtensionsRegistry.getAll();
|
||||||
const chrome = core.chrome as InternalChromeStart;
|
const chrome = core.chrome as InternalChromeStart;
|
||||||
const activeSpace$ = spaces?.getActiveSpace$() ?? of(undefined);
|
const activeSpace$: Observable<Space | undefined> = spaces?.getActiveSpace$() ?? of(undefined);
|
||||||
|
const onCloud = cloud !== undefined; // The new side nav will initially only be available to cloud users
|
||||||
|
const isServerless = this.initializerContext.env.packageInfo.buildFlavor === 'serverless';
|
||||||
|
|
||||||
|
this.isSolutionNavEnabled = onCloud && !isServerless;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
*
|
*
|
||||||
|
@ -95,26 +89,14 @@ export class NavigationPublicPlugin
|
||||||
return createTopNav(customUnifiedSearch ?? unifiedSearch, customExtensions ?? extensions);
|
return createTopNav(customUnifiedSearch ?? unifiedSearch, customExtensions ?? extensions);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCloud = cloud !== undefined; // The new side nav will initially only be available to cloud users
|
|
||||||
const isServerless = this.initializerContext.env.packageInfo.buildFlavor === 'serverless';
|
|
||||||
|
|
||||||
if (cloudExperiments && onCloud && !isServerless) {
|
|
||||||
this.isSolutionNavExperiementEnabled$ = from(
|
|
||||||
cloudExperiments.getVariation(SOLUTION_NAV_FEATURE_FLAG_NAME, false).catch(() => false)
|
|
||||||
).pipe(shareReplay(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the solution navigation if it is enabled
|
// Initialize the solution navigation if it is enabled
|
||||||
combineLatest([this.isSolutionNavExperiementEnabled$, activeSpace$])
|
activeSpace$.pipe(take(1)).subscribe((activeSpace) => {
|
||||||
.pipe(take(1))
|
|
||||||
.subscribe(([isEnabled, activeSpace]) => {
|
|
||||||
this.initiateChromeStyleAndSideNav(chrome, {
|
this.initiateChromeStyleAndSideNav(chrome, {
|
||||||
isFeatureEnabled: isEnabled,
|
|
||||||
isServerless,
|
isServerless,
|
||||||
activeSpace,
|
activeSpace,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isEnabled) return;
|
if (!this.isSolutionNavEnabled) return;
|
||||||
|
|
||||||
chrome.project.setCloudUrls(cloud!);
|
chrome.project.setCloudUrls(cloud!);
|
||||||
});
|
});
|
||||||
|
@ -126,17 +108,12 @@ export class NavigationPublicPlugin
|
||||||
createTopNavWithCustomContext: createCustomTopNav,
|
createTopNavWithCustomContext: createCustomTopNav,
|
||||||
},
|
},
|
||||||
addSolutionNavigation: (solutionNavigation) => {
|
addSolutionNavigation: (solutionNavigation) => {
|
||||||
firstValueFrom(this.isSolutionNavExperiementEnabled$).then((isEnabled) => {
|
if (!this.isSolutionNavEnabled) return;
|
||||||
if (!isEnabled) return;
|
|
||||||
this.addSolutionNavigation(solutionNavigation);
|
this.addSolutionNavigation(solutionNavigation);
|
||||||
});
|
|
||||||
},
|
},
|
||||||
isSolutionNavEnabled$: combineLatest([
|
isSolutionNavEnabled$: activeSpace$.pipe(
|
||||||
this.isSolutionNavExperiementEnabled$,
|
map((activeSpace) => {
|
||||||
activeSpace$,
|
return this.isSolutionNavEnabled && getIsProjectNav(activeSpace?.solution);
|
||||||
]).pipe(
|
|
||||||
map(([isFeatureEnabled, activeSpace]) => {
|
|
||||||
return getIsProjectNav(isFeatureEnabled, activeSpace?.solution) && !isServerless;
|
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
@ -181,14 +158,10 @@ export class NavigationPublicPlugin
|
||||||
|
|
||||||
private initiateChromeStyleAndSideNav(
|
private initiateChromeStyleAndSideNav(
|
||||||
chrome: InternalChromeStart,
|
chrome: InternalChromeStart,
|
||||||
{
|
{ isServerless, activeSpace }: { isServerless: boolean; activeSpace?: Space }
|
||||||
isFeatureEnabled,
|
|
||||||
isServerless,
|
|
||||||
activeSpace,
|
|
||||||
}: { isFeatureEnabled: boolean; isServerless: boolean; activeSpace?: Space }
|
|
||||||
) {
|
) {
|
||||||
const solutionView = activeSpace?.solution;
|
const solutionView = activeSpace?.solution;
|
||||||
const isProjectNav = getIsProjectNav(isFeatureEnabled, solutionView) && !isServerless;
|
const isProjectNav = this.isSolutionNavEnabled && getIsProjectNav(solutionView);
|
||||||
|
|
||||||
// On serverless the chrome style is already set by the serverless plugin
|
// On serverless the chrome style is already set by the serverless plugin
|
||||||
if (!isServerless) {
|
if (!isServerless) {
|
||||||
|
@ -201,8 +174,8 @@ export class NavigationPublicPlugin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIsProjectNav(isFeatureEnabled: boolean, solutionView?: string) {
|
function getIsProjectNav(solutionView?: string) {
|
||||||
return isFeatureEnabled && Boolean(solutionView) && isKnownSolutionView(solutionView);
|
return Boolean(solutionView) && isKnownSolutionView(solutionView);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isKnownSolutionView(solution?: string) {
|
function isKnownSolutionView(solution?: string) {
|
||||||
|
|
|
@ -12,7 +12,6 @@ import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/
|
||||||
import type { SolutionNavigationDefinition } from '@kbn/core-chrome-browser';
|
import type { SolutionNavigationDefinition } from '@kbn/core-chrome-browser';
|
||||||
import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public';
|
import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public';
|
||||||
import type { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/public';
|
import type { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/public';
|
||||||
import type { CloudExperimentsPluginStart } from '@kbn/cloud-experiments-plugin/common';
|
|
||||||
|
|
||||||
import { PanelContentProvider } from '@kbn/shared-ux-chrome-navigation';
|
import { PanelContentProvider } from '@kbn/shared-ux-chrome-navigation';
|
||||||
import { TopNavMenuProps, TopNavMenuExtensionsRegistrySetup, createTopNav } from './top_nav_menu';
|
import { TopNavMenuProps, TopNavMenuExtensionsRegistrySetup, createTopNav } from './top_nav_menu';
|
||||||
|
@ -53,7 +52,6 @@ export interface NavigationPublicSetupDependencies {
|
||||||
export interface NavigationPublicStartDependencies {
|
export interface NavigationPublicStartDependencies {
|
||||||
unifiedSearch: UnifiedSearchPublicPluginStart;
|
unifiedSearch: UnifiedSearchPublicPluginStart;
|
||||||
cloud?: CloudStart;
|
cloud?: CloudStart;
|
||||||
cloudExperiments?: CloudExperimentsPluginStart;
|
|
||||||
spaces?: SpacesPluginStart;
|
spaces?: SpacesPluginStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
import type { CloudExperimentsPluginStart } from '@kbn/cloud-experiments-plugin/common';
|
|
||||||
import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/server';
|
import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/server';
|
||||||
import type { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/server';
|
import type { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/server';
|
||||||
|
|
||||||
|
@ -21,7 +20,6 @@ export interface NavigationServerSetupDependencies {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NavigationServerStartDependencies {
|
export interface NavigationServerStartDependencies {
|
||||||
cloudExperiments?: CloudExperimentsPluginStart;
|
|
||||||
cloud?: CloudStart;
|
cloud?: CloudStart;
|
||||||
spaces?: SpacesPluginStart;
|
spaces?: SpacesPluginStart;
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,6 @@
|
||||||
"@kbn/shared-ux-chrome-navigation",
|
"@kbn/shared-ux-chrome-navigation",
|
||||||
"@kbn/cloud-plugin",
|
"@kbn/cloud-plugin",
|
||||||
"@kbn/config",
|
"@kbn/config",
|
||||||
"@kbn/cloud-experiments-plugin",
|
|
||||||
"@kbn/spaces-plugin",
|
"@kbn/spaces-plugin",
|
||||||
"@kbn/core-ui-settings-common",
|
"@kbn/core-ui-settings-common",
|
||||||
"@kbn/config-schema",
|
"@kbn/config-schema",
|
||||||
|
|
|
@ -328,6 +328,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
|
||||||
'xpack.security.roleManagementEnabled (any)',
|
'xpack.security.roleManagementEnabled (any)',
|
||||||
'xpack.spaces.maxSpaces (number)',
|
'xpack.spaces.maxSpaces (number)',
|
||||||
'xpack.spaces.allowFeatureVisibility (any)',
|
'xpack.spaces.allowFeatureVisibility (any)',
|
||||||
|
'xpack.spaces.allowSolutionVisibility (any)',
|
||||||
'xpack.securitySolution.enableExperimental (array)',
|
'xpack.securitySolution.enableExperimental (array)',
|
||||||
'xpack.securitySolution.prebuiltRulesPackageVersion (string)',
|
'xpack.securitySolution.prebuiltRulesPackageVersion (string)',
|
||||||
'xpack.securitySolution.offeringSettings (record)',
|
'xpack.securitySolution.offeringSettings (record)',
|
||||||
|
|
|
@ -36,10 +36,6 @@ export enum FEATURE_FLAG_NAMES {
|
||||||
* Options are: `true` and `false`.
|
* Options are: `true` and `false`.
|
||||||
*/
|
*/
|
||||||
'observability_onboarding.experimental_onboarding_flow_enabled' = 'observability_onboarding.experimental_onboarding_flow_enabled',
|
'observability_onboarding.experimental_onboarding_flow_enabled' = 'observability_onboarding.experimental_onboarding_flow_enabled',
|
||||||
/**
|
|
||||||
* Used to enable the new stack navigation around solutions during the rollout period.
|
|
||||||
*/
|
|
||||||
'solutionNavEnabled' = 'solutionNavEnabled',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -20,7 +20,6 @@
|
||||||
"management",
|
"management",
|
||||||
"usageCollection",
|
"usageCollection",
|
||||||
"cloud",
|
"cloud",
|
||||||
"cloudExperiments"
|
|
||||||
],
|
],
|
||||||
"requiredBundles": [
|
"requiredBundles": [
|
||||||
"esUiShared",
|
"esUiShared",
|
||||||
|
|
|
@ -8,4 +8,5 @@
|
||||||
export interface ConfigType {
|
export interface ConfigType {
|
||||||
maxSpaces: number;
|
maxSpaces: number;
|
||||||
allowFeatureVisibility: boolean;
|
allowFeatureVisibility: boolean;
|
||||||
|
allowSolutionVisibility: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export { isSolutionNavEnabled } from './is_solution_nav_enabled';
|
|
|
@ -1,20 +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 { CloudExperimentsPluginStart } from '@kbn/cloud-experiments-plugin/common';
|
|
||||||
import type { CloudStart } from '@kbn/cloud-plugin/public';
|
|
||||||
|
|
||||||
const SOLUTION_NAV_FEATURE_FLAG_NAME = 'solutionNavEnabled';
|
|
||||||
|
|
||||||
export const isSolutionNavEnabled = (
|
|
||||||
cloud?: CloudStart,
|
|
||||||
cloudExperiments?: CloudExperimentsPluginStart
|
|
||||||
) => {
|
|
||||||
return Boolean(cloud?.isCloudEnabled) && cloudExperiments
|
|
||||||
? cloudExperiments.getVariation(SOLUTION_NAV_FEATURE_FLAG_NAME, false)
|
|
||||||
: Promise.resolve(false);
|
|
||||||
};
|
|
|
@ -83,6 +83,7 @@ describe('ManageSpacePage', () => {
|
||||||
}}
|
}}
|
||||||
eventTracker={eventTracker}
|
eventTracker={eventTracker}
|
||||||
allowFeatureVisibility
|
allowFeatureVisibility
|
||||||
|
allowSolutionVisibility
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -112,7 +113,7 @@ describe('ManageSpacePage', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows solution view select when enabled', async () => {
|
it('shows solution view select when visible', async () => {
|
||||||
const spacesManager = spacesManagerMock.create();
|
const spacesManager = spacesManagerMock.create();
|
||||||
spacesManager.createSpace = jest.fn(spacesManager.createSpace);
|
spacesManager.createSpace = jest.fn(spacesManager.createSpace);
|
||||||
spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space);
|
spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space);
|
||||||
|
@ -130,7 +131,7 @@ describe('ManageSpacePage', () => {
|
||||||
spaces: { manage: true },
|
spaces: { manage: true },
|
||||||
}}
|
}}
|
||||||
allowFeatureVisibility
|
allowFeatureVisibility
|
||||||
solutionNavExperiment={Promise.resolve(true)}
|
allowSolutionVisibility
|
||||||
eventTracker={eventTracker}
|
eventTracker={eventTracker}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -143,12 +144,11 @@ describe('ManageSpacePage', () => {
|
||||||
expect(findTestSubject(wrapper, 'navigationPanel')).toHaveLength(1);
|
expect(findTestSubject(wrapper, 'navigationPanel')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hides solution view select when not enabled or undefined', async () => {
|
it('hides solution view select when not visible', async () => {
|
||||||
const spacesManager = spacesManagerMock.create();
|
const spacesManager = spacesManagerMock.create();
|
||||||
spacesManager.createSpace = jest.fn(spacesManager.createSpace);
|
spacesManager.createSpace = jest.fn(spacesManager.createSpace);
|
||||||
spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space);
|
spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space);
|
||||||
|
|
||||||
{
|
|
||||||
const wrapper = mountWithIntl(
|
const wrapper = mountWithIntl(
|
||||||
<ManageSpacePage
|
<ManageSpacePage
|
||||||
spacesManager={spacesManager as unknown as SpacesManager}
|
spacesManager={spacesManager as unknown as SpacesManager}
|
||||||
|
@ -162,6 +162,7 @@ describe('ManageSpacePage', () => {
|
||||||
spaces: { manage: true },
|
spaces: { manage: true },
|
||||||
}}
|
}}
|
||||||
allowFeatureVisibility
|
allowFeatureVisibility
|
||||||
|
allowSolutionVisibility={false}
|
||||||
eventTracker={eventTracker}
|
eventTracker={eventTracker}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -172,34 +173,6 @@ describe('ManageSpacePage', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(findTestSubject(wrapper, 'navigationPanel')).toHaveLength(0);
|
expect(findTestSubject(wrapper, 'navigationPanel')).toHaveLength(0);
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const wrapper = mountWithIntl(
|
|
||||||
<ManageSpacePage
|
|
||||||
spacesManager={spacesManager as unknown as SpacesManager}
|
|
||||||
getFeatures={featuresStart.getFeatures}
|
|
||||||
notifications={notificationServiceMock.createStartContract()}
|
|
||||||
history={history}
|
|
||||||
capabilities={{
|
|
||||||
navLinks: {},
|
|
||||||
management: {},
|
|
||||||
catalogue: {},
|
|
||||||
spaces: { manage: true },
|
|
||||||
}}
|
|
||||||
allowFeatureVisibility
|
|
||||||
solutionNavExperiment={Promise.resolve(false)}
|
|
||||||
eventTracker={eventTracker}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
wrapper.update();
|
|
||||||
expect(wrapper.find('input[name="name"]')).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(findTestSubject(wrapper, 'navigationPanel')).toHaveLength(0);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows feature visibility controls when allowed', async () => {
|
it('shows feature visibility controls when allowed', async () => {
|
||||||
|
@ -221,6 +194,7 @@ describe('ManageSpacePage', () => {
|
||||||
}}
|
}}
|
||||||
eventTracker={eventTracker}
|
eventTracker={eventTracker}
|
||||||
allowFeatureVisibility
|
allowFeatureVisibility
|
||||||
|
allowSolutionVisibility
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -251,6 +225,7 @@ describe('ManageSpacePage', () => {
|
||||||
}}
|
}}
|
||||||
eventTracker={eventTracker}
|
eventTracker={eventTracker}
|
||||||
allowFeatureVisibility={false}
|
allowFeatureVisibility={false}
|
||||||
|
allowSolutionVisibility
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -297,7 +272,7 @@ describe('ManageSpacePage', () => {
|
||||||
}}
|
}}
|
||||||
eventTracker={eventTracker}
|
eventTracker={eventTracker}
|
||||||
allowFeatureVisibility
|
allowFeatureVisibility
|
||||||
solutionNavExperiment={Promise.resolve(true)}
|
allowSolutionVisibility
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -375,6 +350,7 @@ describe('ManageSpacePage', () => {
|
||||||
}}
|
}}
|
||||||
eventTracker={eventTracker}
|
eventTracker={eventTracker}
|
||||||
allowFeatureVisibility
|
allowFeatureVisibility
|
||||||
|
allowSolutionVisibility
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -425,6 +401,7 @@ describe('ManageSpacePage', () => {
|
||||||
}}
|
}}
|
||||||
eventTracker={eventTracker}
|
eventTracker={eventTracker}
|
||||||
allowFeatureVisibility
|
allowFeatureVisibility
|
||||||
|
allowSolutionVisibility
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -463,6 +440,7 @@ describe('ManageSpacePage', () => {
|
||||||
}}
|
}}
|
||||||
eventTracker={eventTracker}
|
eventTracker={eventTracker}
|
||||||
allowFeatureVisibility
|
allowFeatureVisibility
|
||||||
|
allowSolutionVisibility
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -525,6 +503,7 @@ describe('ManageSpacePage', () => {
|
||||||
}}
|
}}
|
||||||
eventTracker={eventTracker}
|
eventTracker={eventTracker}
|
||||||
allowFeatureVisibility
|
allowFeatureVisibility
|
||||||
|
allowSolutionVisibility
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,7 @@ interface Props {
|
||||||
capabilities: Capabilities;
|
capabilities: Capabilities;
|
||||||
history: ScopedHistory;
|
history: ScopedHistory;
|
||||||
allowFeatureVisibility: boolean;
|
allowFeatureVisibility: boolean;
|
||||||
solutionNavExperiment?: Promise<boolean>;
|
allowSolutionVisibility: boolean;
|
||||||
eventTracker: EventTracker;
|
eventTracker: EventTracker;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +74,6 @@ interface State {
|
||||||
isInvalid: boolean;
|
isInvalid: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
isSolutionNavEnabled: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ManageSpacePage extends Component<Props, State> {
|
export class ManageSpacePage extends Component<Props, State> {
|
||||||
|
@ -91,7 +90,6 @@ export class ManageSpacePage extends Component<Props, State> {
|
||||||
color: getSpaceColor({}),
|
color: getSpaceColor({}),
|
||||||
},
|
},
|
||||||
features: [],
|
features: [],
|
||||||
isSolutionNavEnabled: false,
|
|
||||||
haveDisabledFeaturesChanged: false,
|
haveDisabledFeaturesChanged: false,
|
||||||
hasSolutionViewChanged: false,
|
hasSolutionViewChanged: false,
|
||||||
};
|
};
|
||||||
|
@ -118,10 +116,6 @@ export class ManageSpacePage extends Component<Props, State> {
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.solutionNavExperiment?.then((isEnabled) => {
|
|
||||||
this.setState({ isSolutionNavEnabled: isEnabled });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async componentDidUpdate(previousProps: Props, prevState: State) {
|
public async componentDidUpdate(previousProps: Props, prevState: State) {
|
||||||
|
@ -201,7 +195,7 @@ export class ManageSpacePage extends Component<Props, State> {
|
||||||
validator={this.validator}
|
validator={this.validator}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{this.state.isSolutionNavEnabled && (
|
{!!this.props.allowSolutionVisibility && (
|
||||||
<>
|
<>
|
||||||
<EuiSpacer size="l" />
|
<EuiSpacer size="l" />
|
||||||
<SolutionView space={this.state.space} onChange={this.onSpaceChange} />
|
<SolutionView space={this.state.space} onChange={this.onSpaceChange} />
|
||||||
|
|
|
@ -23,6 +23,7 @@ describe('ManagementService', () => {
|
||||||
const config: ConfigType = {
|
const config: ConfigType = {
|
||||||
maxSpaces: 1000,
|
maxSpaces: 1000,
|
||||||
allowFeatureVisibility: true,
|
allowFeatureVisibility: true,
|
||||||
|
allowSolutionVisibility: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('#setup', () => {
|
describe('#setup', () => {
|
||||||
|
@ -41,7 +42,6 @@ describe('ManagementService', () => {
|
||||||
spacesManager: spacesManagerMock.create(),
|
spacesManager: spacesManagerMock.create(),
|
||||||
config,
|
config,
|
||||||
getRolesAPIClient: getRolesAPIClientMock,
|
getRolesAPIClient: getRolesAPIClientMock,
|
||||||
solutionNavExperiment: Promise.resolve(false),
|
|
||||||
eventTracker,
|
eventTracker,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -63,7 +63,6 @@ describe('ManagementService', () => {
|
||||||
spacesManager: spacesManagerMock.create(),
|
spacesManager: spacesManagerMock.create(),
|
||||||
config,
|
config,
|
||||||
getRolesAPIClient: getRolesAPIClientMock,
|
getRolesAPIClient: getRolesAPIClientMock,
|
||||||
solutionNavExperiment: Promise.resolve(false),
|
|
||||||
eventTracker,
|
eventTracker,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -86,7 +85,6 @@ describe('ManagementService', () => {
|
||||||
spacesManager: spacesManagerMock.create(),
|
spacesManager: spacesManagerMock.create(),
|
||||||
config,
|
config,
|
||||||
getRolesAPIClient: jest.fn(),
|
getRolesAPIClient: jest.fn(),
|
||||||
solutionNavExperiment: Promise.resolve(false),
|
|
||||||
eventTracker,
|
eventTracker,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,6 @@ interface SetupDeps {
|
||||||
spacesManager: SpacesManager;
|
spacesManager: SpacesManager;
|
||||||
config: ConfigType;
|
config: ConfigType;
|
||||||
getRolesAPIClient: () => Promise<RolesAPIClient>;
|
getRolesAPIClient: () => Promise<RolesAPIClient>;
|
||||||
solutionNavExperiment: Promise<boolean>;
|
|
||||||
eventTracker: EventTracker;
|
eventTracker: EventTracker;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +33,6 @@ export class ManagementService {
|
||||||
spacesManager,
|
spacesManager,
|
||||||
config,
|
config,
|
||||||
getRolesAPIClient,
|
getRolesAPIClient,
|
||||||
solutionNavExperiment,
|
|
||||||
eventTracker,
|
eventTracker,
|
||||||
}: SetupDeps) {
|
}: SetupDeps) {
|
||||||
this.registeredSpacesManagementApp = management.sections.section.kibana.registerApp(
|
this.registeredSpacesManagementApp = management.sections.section.kibana.registerApp(
|
||||||
|
@ -43,7 +41,6 @@ export class ManagementService {
|
||||||
spacesManager,
|
spacesManager,
|
||||||
config,
|
config,
|
||||||
getRolesAPIClient,
|
getRolesAPIClient,
|
||||||
solutionNavExperiment,
|
|
||||||
eventTracker,
|
eventTracker,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -84,6 +84,7 @@ describe('SpacesGridPage', () => {
|
||||||
catalogue: {},
|
catalogue: {},
|
||||||
spaces: { manage: true },
|
spaces: { manage: true },
|
||||||
}}
|
}}
|
||||||
|
allowSolutionVisibility={false}
|
||||||
{...spacesGridCommonProps}
|
{...spacesGridCommonProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -143,7 +144,7 @@ describe('SpacesGridPage', () => {
|
||||||
catalogue: {},
|
catalogue: {},
|
||||||
spaces: { manage: true },
|
spaces: { manage: true },
|
||||||
}}
|
}}
|
||||||
solutionNavExperiment={Promise.resolve(true)}
|
allowSolutionVisibility
|
||||||
{...spacesGridCommonProps}
|
{...spacesGridCommonProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -179,7 +180,7 @@ describe('SpacesGridPage', () => {
|
||||||
catalogue: {},
|
catalogue: {},
|
||||||
spaces: { manage: true },
|
spaces: { manage: true },
|
||||||
}}
|
}}
|
||||||
solutionNavExperiment={Promise.resolve(true)}
|
allowSolutionVisibility
|
||||||
{...spacesGridCommonProps}
|
{...spacesGridCommonProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -212,7 +213,7 @@ describe('SpacesGridPage', () => {
|
||||||
catalogue: {},
|
catalogue: {},
|
||||||
spaces: { manage: true },
|
spaces: { manage: true },
|
||||||
}}
|
}}
|
||||||
solutionNavExperiment={Promise.resolve(true)}
|
allowSolutionVisibility
|
||||||
{...spacesGridCommonProps}
|
{...spacesGridCommonProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -244,7 +245,7 @@ describe('SpacesGridPage', () => {
|
||||||
catalogue: {},
|
catalogue: {},
|
||||||
spaces: { manage: true },
|
spaces: { manage: true },
|
||||||
}}
|
}}
|
||||||
solutionNavExperiment={Promise.resolve(true)}
|
allowSolutionVisibility
|
||||||
{...spacesGridCommonProps}
|
{...spacesGridCommonProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -275,6 +276,7 @@ describe('SpacesGridPage', () => {
|
||||||
catalogue: {},
|
catalogue: {},
|
||||||
spaces: { manage: true },
|
spaces: { manage: true },
|
||||||
}}
|
}}
|
||||||
|
allowSolutionVisibility
|
||||||
{...spacesGridCommonProps}
|
{...spacesGridCommonProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -305,6 +307,7 @@ describe('SpacesGridPage', () => {
|
||||||
spaces: { manage: true },
|
spaces: { manage: true },
|
||||||
}}
|
}}
|
||||||
maxSpaces={1}
|
maxSpaces={1}
|
||||||
|
allowSolutionVisibility
|
||||||
serverBasePath={spacesGridCommonProps.serverBasePath}
|
serverBasePath={spacesGridCommonProps.serverBasePath}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -339,6 +342,7 @@ describe('SpacesGridPage', () => {
|
||||||
catalogue: {},
|
catalogue: {},
|
||||||
spaces: { manage: true },
|
spaces: { manage: true },
|
||||||
}}
|
}}
|
||||||
|
allowSolutionVisibility
|
||||||
{...spacesGridCommonProps}
|
{...spacesGridCommonProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -374,6 +378,7 @@ describe('SpacesGridPage', () => {
|
||||||
catalogue: {},
|
catalogue: {},
|
||||||
spaces: { manage: true },
|
spaces: { manage: true },
|
||||||
}}
|
}}
|
||||||
|
allowSolutionVisibility
|
||||||
{...spacesGridCommonProps}
|
{...spacesGridCommonProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -57,7 +57,7 @@ interface Props {
|
||||||
history: ScopedHistory;
|
history: ScopedHistory;
|
||||||
getUrlForApp: ApplicationStart['getUrlForApp'];
|
getUrlForApp: ApplicationStart['getUrlForApp'];
|
||||||
maxSpaces: number;
|
maxSpaces: number;
|
||||||
solutionNavExperiment?: Promise<boolean>;
|
allowSolutionVisibility: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
@ -67,7 +67,6 @@ interface State {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
showConfirmDeleteModal: boolean;
|
showConfirmDeleteModal: boolean;
|
||||||
selectedSpace: Space | null;
|
selectedSpace: Space | null;
|
||||||
isSolutionNavEnabled: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SpacesGridPage extends Component<Props, State> {
|
export class SpacesGridPage extends Component<Props, State> {
|
||||||
|
@ -80,7 +79,6 @@ export class SpacesGridPage extends Component<Props, State> {
|
||||||
loading: true,
|
loading: true,
|
||||||
showConfirmDeleteModal: false,
|
showConfirmDeleteModal: false,
|
||||||
selectedSpace: null,
|
selectedSpace: null,
|
||||||
isSolutionNavEnabled: false,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,10 +86,6 @@ export class SpacesGridPage extends Component<Props, State> {
|
||||||
if (this.props.capabilities.spaces.manage) {
|
if (this.props.capabilities.spaces.manage) {
|
||||||
this.loadGrid();
|
this.loadGrid();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.solutionNavExperiment?.then((isEnabled) => {
|
|
||||||
this.setState({ isSolutionNavEnabled: isEnabled });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
@ -367,7 +361,7 @@ export class SpacesGridPage extends Component<Props, State> {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (this.state.isSolutionNavEnabled) {
|
if (this.props.allowSolutionVisibility) {
|
||||||
config.push({
|
config.push({
|
||||||
field: 'solution',
|
field: 'solution',
|
||||||
name: i18n.translate('xpack.spaces.management.spacesGridPage.solutionColumnName', {
|
name: i18n.translate('xpack.spaces.management.spacesGridPage.solutionColumnName', {
|
||||||
|
|
|
@ -30,6 +30,7 @@ import { spacesManagerMock } from '../spaces_manager/mocks';
|
||||||
const config: ConfigType = {
|
const config: ConfigType = {
|
||||||
maxSpaces: 1000,
|
maxSpaces: 1000,
|
||||||
allowFeatureVisibility: true,
|
allowFeatureVisibility: true,
|
||||||
|
allowSolutionVisibility: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const eventTracker = new EventTracker({ reportEvent: jest.fn() });
|
const eventTracker = new EventTracker({ reportEvent: jest.fn() });
|
||||||
|
@ -56,7 +57,6 @@ async function mountApp(basePath: string, pathname: string, spaceId?: string) {
|
||||||
getStartServices: async () => [coreStart, pluginsStart as PluginsStart, {}],
|
getStartServices: async () => [coreStart, pluginsStart as PluginsStart, {}],
|
||||||
config,
|
config,
|
||||||
getRolesAPIClient: jest.fn(),
|
getRolesAPIClient: jest.fn(),
|
||||||
solutionNavExperiment: Promise.resolve(false),
|
|
||||||
eventTracker,
|
eventTracker,
|
||||||
})
|
})
|
||||||
.mount({
|
.mount({
|
||||||
|
@ -79,7 +79,6 @@ describe('spacesManagementApp', () => {
|
||||||
getStartServices: coreMock.createSetup().getStartServices as any,
|
getStartServices: coreMock.createSetup().getStartServices as any,
|
||||||
config,
|
config,
|
||||||
getRolesAPIClient: jest.fn(),
|
getRolesAPIClient: jest.fn(),
|
||||||
solutionNavExperiment: Promise.resolve(false),
|
|
||||||
eventTracker,
|
eventTracker,
|
||||||
})
|
})
|
||||||
).toMatchInlineSnapshot(`
|
).toMatchInlineSnapshot(`
|
||||||
|
@ -105,7 +104,7 @@ describe('spacesManagementApp', () => {
|
||||||
css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)."
|
css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)."
|
||||||
data-test-subj="kbnRedirectAppLink"
|
data-test-subj="kbnRedirectAppLink"
|
||||||
>
|
>
|
||||||
Spaces Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{}},"serverBasePath":"","history":{"action":"PUSH","length":1,"location":{"pathname":"/","search":"","hash":""}},"maxSpaces":1000,"solutionNavExperiment":{}}
|
Spaces Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{}},"serverBasePath":"","history":{"action":"PUSH","length":1,"location":{"pathname":"/","search":"","hash":""}},"maxSpaces":1000,"allowSolutionVisibility":true}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
@ -132,7 +131,7 @@ describe('spacesManagementApp', () => {
|
||||||
css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)."
|
css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)."
|
||||||
data-test-subj="kbnRedirectAppLink"
|
data-test-subj="kbnRedirectAppLink"
|
||||||
>
|
>
|
||||||
Spaces Edit Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/create","search":"","hash":""}},"allowFeatureVisibility":true,"solutionNavExperiment":{},"eventTracker":{"analytics":{}}}
|
Spaces Edit Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/create","search":"","hash":""}},"allowFeatureVisibility":true,"allowSolutionVisibility":true,"eventTracker":{"analytics":{}}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
@ -165,7 +164,7 @@ describe('spacesManagementApp', () => {
|
||||||
css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)."
|
css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)."
|
||||||
data-test-subj="kbnRedirectAppLink"
|
data-test-subj="kbnRedirectAppLink"
|
||||||
>
|
>
|
||||||
Spaces Edit Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{}},"spaceId":"some-space","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/some-space","search":"","hash":""}},"allowFeatureVisibility":true,"solutionNavExperiment":{},"eventTracker":{"analytics":{}}}
|
Spaces Edit Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{}},"spaceId":"some-space","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/some-space","search":"","hash":""}},"allowFeatureVisibility":true,"allowSolutionVisibility":true,"eventTracker":{"analytics":{}}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
|
@ -29,19 +29,12 @@ interface CreateParams {
|
||||||
spacesManager: SpacesManager;
|
spacesManager: SpacesManager;
|
||||||
config: ConfigType;
|
config: ConfigType;
|
||||||
getRolesAPIClient: () => Promise<RolesAPIClient>;
|
getRolesAPIClient: () => Promise<RolesAPIClient>;
|
||||||
solutionNavExperiment: Promise<boolean>;
|
|
||||||
eventTracker: EventTracker;
|
eventTracker: EventTracker;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const spacesManagementApp = Object.freeze({
|
export const spacesManagementApp = Object.freeze({
|
||||||
id: 'spaces',
|
id: 'spaces',
|
||||||
create({
|
create({ getStartServices, spacesManager, config, eventTracker }: CreateParams) {
|
||||||
getStartServices,
|
|
||||||
spacesManager,
|
|
||||||
config,
|
|
||||||
solutionNavExperiment,
|
|
||||||
eventTracker,
|
|
||||||
}: CreateParams) {
|
|
||||||
const title = i18n.translate('xpack.spaces.displayName', {
|
const title = i18n.translate('xpack.spaces.displayName', {
|
||||||
defaultMessage: 'Spaces',
|
defaultMessage: 'Spaces',
|
||||||
});
|
});
|
||||||
|
@ -75,7 +68,7 @@ export const spacesManagementApp = Object.freeze({
|
||||||
history={history}
|
history={history}
|
||||||
getUrlForApp={application.getUrlForApp}
|
getUrlForApp={application.getUrlForApp}
|
||||||
maxSpaces={config.maxSpaces}
|
maxSpaces={config.maxSpaces}
|
||||||
solutionNavExperiment={solutionNavExperiment}
|
allowSolutionVisibility={config.allowSolutionVisibility}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -98,7 +91,7 @@ export const spacesManagementApp = Object.freeze({
|
||||||
spacesManager={spacesManager}
|
spacesManager={spacesManager}
|
||||||
history={history}
|
history={history}
|
||||||
allowFeatureVisibility={config.allowFeatureVisibility}
|
allowFeatureVisibility={config.allowFeatureVisibility}
|
||||||
solutionNavExperiment={solutionNavExperiment}
|
allowSolutionVisibility={config.allowSolutionVisibility}
|
||||||
eventTracker={eventTracker}
|
eventTracker={eventTracker}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -126,7 +119,7 @@ export const spacesManagementApp = Object.freeze({
|
||||||
onLoadSpace={onLoadSpace}
|
onLoadSpace={onLoadSpace}
|
||||||
history={history}
|
history={history}
|
||||||
allowFeatureVisibility={config.allowFeatureVisibility}
|
allowFeatureVisibility={config.allowFeatureVisibility}
|
||||||
solutionNavExperiment={solutionNavExperiment}
|
allowSolutionVisibility={config.allowSolutionVisibility}
|
||||||
eventTracker={eventTracker}
|
eventTracker={eventTracker}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -48,7 +48,7 @@ interface Props {
|
||||||
navigateToApp: ApplicationStart['navigateToApp'];
|
navigateToApp: ApplicationStart['navigateToApp'];
|
||||||
navigateToUrl: ApplicationStart['navigateToUrl'];
|
navigateToUrl: ApplicationStart['navigateToUrl'];
|
||||||
readonly activeSpace: Space | null;
|
readonly activeSpace: Space | null;
|
||||||
isSolutionNavEnabled: boolean;
|
allowSolutionVisibility: boolean;
|
||||||
eventTracker: EventTracker;
|
eventTracker: EventTracker;
|
||||||
}
|
}
|
||||||
class SpacesMenuUI extends Component<Props> {
|
class SpacesMenuUI extends Component<Props> {
|
||||||
|
@ -97,7 +97,7 @@ class SpacesMenuUI extends Component<Props> {
|
||||||
id={this.props.id}
|
id={this.props.id}
|
||||||
className={'spcMenu'}
|
className={'spcMenu'}
|
||||||
title={i18n.translate('xpack.spaces.navControl.spacesMenu.changeCurrentSpaceTitle', {
|
title={i18n.translate('xpack.spaces.navControl.spacesMenu.changeCurrentSpaceTitle', {
|
||||||
defaultMessage: 'Change current space xx',
|
defaultMessage: 'Change current space',
|
||||||
})}
|
})}
|
||||||
{...searchableProps}
|
{...searchableProps}
|
||||||
noMatchesMessage={noSpacesMessage}
|
noMatchesMessage={noSpacesMessage}
|
||||||
|
@ -140,7 +140,7 @@ class SpacesMenuUI extends Component<Props> {
|
||||||
<LazySpaceAvatar space={space} size={'s'} announceSpaceName={false} />
|
<LazySpaceAvatar space={space} size={'s'} announceSpaceName={false} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
),
|
),
|
||||||
...(this.props.isSolutionNavEnabled && {
|
...(this.props.allowSolutionVisibility && {
|
||||||
append: <SpaceSolutionBadge solution={space.solution} />,
|
append: <SpaceSolutionBadge solution={space.solution} />,
|
||||||
}),
|
}),
|
||||||
checked: this.props.activeSpace?.id === space.id ? 'on' : undefined,
|
checked: this.props.activeSpace?.id === space.id ? 'on' : undefined,
|
||||||
|
|
|
@ -13,12 +13,13 @@ import type { CoreStart } from '@kbn/core/public';
|
||||||
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
||||||
|
|
||||||
import type { EventTracker } from '../analytics';
|
import type { EventTracker } from '../analytics';
|
||||||
|
import type { ConfigType } from '../config';
|
||||||
import type { SpacesManager } from '../spaces_manager';
|
import type { SpacesManager } from '../spaces_manager';
|
||||||
|
|
||||||
export function initSpacesNavControl(
|
export function initSpacesNavControl(
|
||||||
spacesManager: SpacesManager,
|
spacesManager: SpacesManager,
|
||||||
core: CoreStart,
|
core: CoreStart,
|
||||||
solutionNavExperiment: Promise<boolean>,
|
config: ConfigType,
|
||||||
eventTracker: EventTracker
|
eventTracker: EventTracker
|
||||||
) {
|
) {
|
||||||
core.chrome.navControls.registerLeft({
|
core.chrome.navControls.registerLeft({
|
||||||
|
@ -44,7 +45,7 @@ export function initSpacesNavControl(
|
||||||
capabilities={core.application.capabilities}
|
capabilities={core.application.capabilities}
|
||||||
navigateToApp={core.application.navigateToApp}
|
navigateToApp={core.application.navigateToApp}
|
||||||
navigateToUrl={core.application.navigateToUrl}
|
navigateToUrl={core.application.navigateToUrl}
|
||||||
solutionNavExperiment={solutionNavExperiment}
|
allowSolutionVisibility={config.allowSolutionVisibility}
|
||||||
eventTracker={eventTracker}
|
eventTracker={eventTracker}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
|
@ -49,7 +49,7 @@ const reportEvent = jest.fn();
|
||||||
const eventTracker = new EventTracker({ reportEvent });
|
const eventTracker = new EventTracker({ reportEvent });
|
||||||
|
|
||||||
describe('NavControlPopover', () => {
|
describe('NavControlPopover', () => {
|
||||||
async function setup(spaces: Space[], isSolutionNavEnabled = false, activeSpace?: Space) {
|
async function setup(spaces: Space[], allowSolutionVisibility = false, activeSpace?: Space) {
|
||||||
const spacesManager = spacesManagerMock.create();
|
const spacesManager = spacesManagerMock.create();
|
||||||
spacesManager.getSpaces = jest.fn().mockResolvedValue(spaces);
|
spacesManager.getSpaces = jest.fn().mockResolvedValue(spaces);
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ describe('NavControlPopover', () => {
|
||||||
capabilities={{ navLinks: {}, management: {}, catalogue: {}, spaces: { manage: true } }}
|
capabilities={{ navLinks: {}, management: {}, catalogue: {}, spaces: { manage: true } }}
|
||||||
navigateToApp={jest.fn()}
|
navigateToApp={jest.fn()}
|
||||||
navigateToUrl={jest.fn()}
|
navigateToUrl={jest.fn()}
|
||||||
solutionNavExperiment={Promise.resolve(isSolutionNavEnabled)}
|
allowSolutionVisibility={allowSolutionVisibility}
|
||||||
eventTracker={eventTracker}
|
eventTracker={eventTracker}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -89,7 +89,7 @@ describe('NavControlPopover', () => {
|
||||||
capabilities={{ navLinks: {}, management: {}, catalogue: {}, spaces: { manage: true } }}
|
capabilities={{ navLinks: {}, management: {}, catalogue: {}, spaces: { manage: true } }}
|
||||||
navigateToApp={jest.fn()}
|
navigateToApp={jest.fn()}
|
||||||
navigateToUrl={jest.fn()}
|
navigateToUrl={jest.fn()}
|
||||||
solutionNavExperiment={Promise.resolve(false)}
|
allowSolutionVisibility={false}
|
||||||
eventTracker={eventTracker}
|
eventTracker={eventTracker}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -115,7 +115,7 @@ describe('NavControlPopover', () => {
|
||||||
capabilities={{ navLinks: {}, management: {}, catalogue: {}, spaces: { manage: true } }}
|
capabilities={{ navLinks: {}, management: {}, catalogue: {}, spaces: { manage: true } }}
|
||||||
navigateToApp={jest.fn()}
|
navigateToApp={jest.fn()}
|
||||||
navigateToUrl={jest.fn()}
|
navigateToUrl={jest.fn()}
|
||||||
solutionNavExperiment={Promise.resolve(false)}
|
allowSolutionVisibility={false}
|
||||||
eventTracker={eventTracker}
|
eventTracker={eventTracker}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -283,7 +283,7 @@ describe('NavControlPopover', () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
const activeSpace = spaces[0];
|
const activeSpace = spaces[0];
|
||||||
const wrapper = await setup(spaces, true /** isSolutionEnabled **/, activeSpace);
|
const wrapper = await setup(spaces, true /** allowSolutionVisibility **/, activeSpace);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find(EuiHeaderSectionItemButton).find('button').simulate('click');
|
wrapper.find(EuiHeaderSectionItemButton).find('button').simulate('click');
|
||||||
|
|
|
@ -38,7 +38,7 @@ interface Props {
|
||||||
navigateToUrl: ApplicationStart['navigateToUrl'];
|
navigateToUrl: ApplicationStart['navigateToUrl'];
|
||||||
serverBasePath: string;
|
serverBasePath: string;
|
||||||
theme: WithEuiThemeProps['theme'];
|
theme: WithEuiThemeProps['theme'];
|
||||||
solutionNavExperiment: Promise<boolean>;
|
allowSolutionVisibility: boolean;
|
||||||
eventTracker: EventTracker;
|
eventTracker: EventTracker;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,7 +47,6 @@ interface State {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
activeSpace: Space | null;
|
activeSpace: Space | null;
|
||||||
spaces: Space[];
|
spaces: Space[];
|
||||||
isSolutionNavEnabled: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const popoutContentId = 'headerSpacesMenuContent';
|
const popoutContentId = 'headerSpacesMenuContent';
|
||||||
|
@ -62,7 +61,6 @@ class NavControlPopoverUI extends Component<Props, State> {
|
||||||
loading: false,
|
loading: false,
|
||||||
activeSpace: null,
|
activeSpace: null,
|
||||||
spaces: [],
|
spaces: [],
|
||||||
isSolutionNavEnabled: false,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,10 +72,6 @@ class NavControlPopoverUI extends Component<Props, State> {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.props.solutionNavExperiment.then((isEnabled) => {
|
|
||||||
this.setState({ isSolutionNavEnabled: isEnabled });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
|
@ -110,7 +104,7 @@ class NavControlPopoverUI extends Component<Props, State> {
|
||||||
navigateToApp={this.props.navigateToApp}
|
navigateToApp={this.props.navigateToApp}
|
||||||
navigateToUrl={this.props.navigateToUrl}
|
navigateToUrl={this.props.navigateToUrl}
|
||||||
activeSpace={this.state.activeSpace}
|
activeSpace={this.state.activeSpace}
|
||||||
isSolutionNavEnabled={this.state.isSolutionNavEnabled}
|
allowSolutionVisibility={this.props.allowSolutionVisibility}
|
||||||
eventTracker={this.props.eventTracker}
|
eventTracker={this.props.eventTracker}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { CloudExperimentsPluginStart } from '@kbn/cloud-experiments-plugin/common';
|
|
||||||
import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public';
|
import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public';
|
||||||
import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public';
|
import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public';
|
||||||
import type { FeaturesPluginStart } from '@kbn/features-plugin/public';
|
import type { FeaturesPluginStart } from '@kbn/features-plugin/public';
|
||||||
|
@ -16,7 +15,6 @@ import type { SecurityPluginStart } from '@kbn/security-plugin-types-public';
|
||||||
import { EventTracker, registerAnalyticsContext, registerSpacesEventTypes } from './analytics';
|
import { EventTracker, registerAnalyticsContext, registerSpacesEventTypes } from './analytics';
|
||||||
import type { ConfigType } from './config';
|
import type { ConfigType } from './config';
|
||||||
import { createSpacesFeatureCatalogueEntry } from './create_feature_catalogue_entry';
|
import { createSpacesFeatureCatalogueEntry } from './create_feature_catalogue_entry';
|
||||||
import { isSolutionNavEnabled } from './experiments';
|
|
||||||
import { ManagementService } from './management';
|
import { ManagementService } from './management';
|
||||||
import { initSpacesNavControl } from './nav_control';
|
import { initSpacesNavControl } from './nav_control';
|
||||||
import { spaceSelectorApp } from './space_selector';
|
import { spaceSelectorApp } from './space_selector';
|
||||||
|
@ -34,7 +32,6 @@ export interface PluginsStart {
|
||||||
features: FeaturesPluginStart;
|
features: FeaturesPluginStart;
|
||||||
management?: ManagementStart;
|
management?: ManagementStart;
|
||||||
cloud?: CloudStart;
|
cloud?: CloudStart;
|
||||||
cloudExperiments?: CloudExperimentsPluginStart;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -53,9 +50,8 @@ export class SpacesPlugin implements Plugin<SpacesPluginSetup, SpacesPluginStart
|
||||||
private eventTracker!: EventTracker;
|
private eventTracker!: EventTracker;
|
||||||
|
|
||||||
private managementService?: ManagementService;
|
private managementService?: ManagementService;
|
||||||
private readonly config: ConfigType;
|
private config: ConfigType;
|
||||||
private readonly isServerless: boolean;
|
private readonly isServerless: boolean;
|
||||||
private solutionNavExperiment = Promise.resolve(false);
|
|
||||||
|
|
||||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||||
this.config = this.initializerContext.config.get<ConfigType>();
|
this.config = this.initializerContext.config.get<ConfigType>();
|
||||||
|
@ -75,18 +71,16 @@ export class SpacesPlugin implements Plugin<SpacesPluginSetup, SpacesPluginStart
|
||||||
hasOnlyDefaultSpace,
|
hasOnlyDefaultSpace,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onCloud = plugins.cloud !== undefined && plugins.cloud.isCloudEnabled;
|
||||||
|
if (!onCloud) {
|
||||||
|
this.config = {
|
||||||
|
...this.config,
|
||||||
|
allowSolutionVisibility: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
registerSpacesEventTypes(core);
|
registerSpacesEventTypes(core);
|
||||||
this.eventTracker = new EventTracker(core.analytics);
|
this.eventTracker = new EventTracker(core.analytics);
|
||||||
|
|
||||||
this.solutionNavExperiment = core
|
|
||||||
.getStartServices()
|
|
||||||
.then(([, { cloud, cloudExperiments }]) => isSolutionNavEnabled(cloud, cloudExperiments))
|
|
||||||
.catch((err) => {
|
|
||||||
this.initializerContext.logger.get().error(`Failed to retrieve cloud experiment: ${err}`);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Only skip setup of space selector and management service if serverless and only one space is allowed
|
// Only skip setup of space selector and management service if serverless and only one space is allowed
|
||||||
if (!(this.isServerless && hasOnlyDefaultSpace)) {
|
if (!(this.isServerless && hasOnlyDefaultSpace)) {
|
||||||
const getRolesAPIClient = async () => {
|
const getRolesAPIClient = async () => {
|
||||||
|
@ -113,7 +107,6 @@ export class SpacesPlugin implements Plugin<SpacesPluginSetup, SpacesPluginStart
|
||||||
spacesManager: this.spacesManager,
|
spacesManager: this.spacesManager,
|
||||||
config: this.config,
|
config: this.config,
|
||||||
getRolesAPIClient,
|
getRolesAPIClient,
|
||||||
solutionNavExperiment: this.solutionNavExperiment,
|
|
||||||
eventTracker: this.eventTracker,
|
eventTracker: this.eventTracker,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -133,7 +126,7 @@ export class SpacesPlugin implements Plugin<SpacesPluginSetup, SpacesPluginStart
|
||||||
public start(core: CoreStart) {
|
public start(core: CoreStart) {
|
||||||
// Only skip spaces navigation if serverless and only one space is allowed
|
// Only skip spaces navigation if serverless and only one space is allowed
|
||||||
if (!(this.isServerless && this.config.maxSpaces === 1)) {
|
if (!(this.isServerless && this.config.maxSpaces === 1)) {
|
||||||
initSpacesNavControl(this.spacesManager, core, this.solutionNavExperiment, this.eventTracker);
|
initSpacesNavControl(this.spacesManager, core, this.config, this.eventTracker);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.spacesApi;
|
return this.spacesApi;
|
||||||
|
|
|
@ -21,6 +21,7 @@ describe('config schema', () => {
|
||||||
expect(ConfigSchema.validate({})).toMatchInlineSnapshot(`
|
expect(ConfigSchema.validate({})).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"allowFeatureVisibility": true,
|
"allowFeatureVisibility": true,
|
||||||
|
"allowSolutionVisibility": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"maxSpaces": 1000,
|
"maxSpaces": 1000,
|
||||||
}
|
}
|
||||||
|
@ -29,6 +30,7 @@ describe('config schema', () => {
|
||||||
expect(ConfigSchema.validate({}, { dev: false })).toMatchInlineSnapshot(`
|
expect(ConfigSchema.validate({}, { dev: false })).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"allowFeatureVisibility": true,
|
"allowFeatureVisibility": true,
|
||||||
|
"allowSolutionVisibility": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"maxSpaces": 1000,
|
"maxSpaces": 1000,
|
||||||
}
|
}
|
||||||
|
@ -37,6 +39,7 @@ describe('config schema', () => {
|
||||||
expect(ConfigSchema.validate({}, { dev: true })).toMatchInlineSnapshot(`
|
expect(ConfigSchema.validate({}, { dev: true })).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"allowFeatureVisibility": true,
|
"allowFeatureVisibility": true,
|
||||||
|
"allowSolutionVisibility": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"maxSpaces": 1000,
|
"maxSpaces": 1000,
|
||||||
}
|
}
|
||||||
|
@ -61,19 +64,36 @@ describe('config schema', () => {
|
||||||
expect(() => ConfigSchema.validate({ allowFeatureVisibility: false }, {})).toThrow();
|
expect(() => ConfigSchema.validate({ allowFeatureVisibility: false }, {})).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not throw error if allowFeatureVisibility is disabled in serverless offering', () => {
|
it('should not throw error if allowFeatureVisibility and allowSolutionVisibility are disabled in serverless offering', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
ConfigSchema.validate({ allowFeatureVisibility: false }, { serverless: true })
|
ConfigSchema.validate(
|
||||||
|
{ allowFeatureVisibility: false, allowSolutionVisibility: false },
|
||||||
|
{ serverless: true }
|
||||||
|
)
|
||||||
).not.toThrow();
|
).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not throw error if allowFeatureVisibility is enabled in classic offering', () => {
|
it('should not throw error if allowFeatureVisibility and allowSolutionVisibility are enabled in classic offering', () => {
|
||||||
expect(() => ConfigSchema.validate({ allowFeatureVisibility: true }, {})).not.toThrow();
|
expect(() =>
|
||||||
|
ConfigSchema.validate({ allowFeatureVisibility: true, allowSolutionVisibility: true }, {})
|
||||||
|
).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error if allowFeatureVisibility is enabled in serverless offering', () => {
|
it('should throw error if allowFeatureVisibility is enabled in serverless offering', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
ConfigSchema.validate({ allowFeatureVisibility: true }, { serverless: true })
|
ConfigSchema.validate(
|
||||||
|
{ allowFeatureVisibility: true, allowSolutionVisibility: false },
|
||||||
|
{ serverless: true }
|
||||||
|
)
|
||||||
|
).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error if allowSolutionVisibility is enabled in serverless offering', () => {
|
||||||
|
expect(() =>
|
||||||
|
ConfigSchema.validate(
|
||||||
|
{ allowSolutionVisibility: true, allowFeatureVisibility: false },
|
||||||
|
{ serverless: true }
|
||||||
|
)
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -40,6 +40,20 @@ export const ConfigSchema = schema.object({
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
allowSolutionVisibility: offeringBasedSchema({
|
||||||
|
serverless: schema.literal(false),
|
||||||
|
traditional: schema.boolean({
|
||||||
|
validate: (rawValue) => {
|
||||||
|
// This setting should not be configurable on-prem to avoid bugs when e.g. existing spaces
|
||||||
|
// have custom solution but admins would be unable to change the navigation solution if the
|
||||||
|
// UI/APIs are disabled.
|
||||||
|
if (rawValue === false) {
|
||||||
|
return 'Solution visibility can only be disabled on serverless';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultValue: true,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function createConfig$(context: PluginInitializerContext) {
|
export function createConfig$(context: PluginInitializerContext) {
|
||||||
|
|
|
@ -33,6 +33,7 @@ export const config: PluginConfigDescriptor = {
|
||||||
exposeToBrowser: {
|
exposeToBrowser: {
|
||||||
maxSpaces: true,
|
maxSpaces: true,
|
||||||
allowFeatureVisibility: true,
|
allowFeatureVisibility: true,
|
||||||
|
allowSolutionVisibility: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
|
import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
|
||||||
import type { CloudSetup } from '@kbn/cloud-plugin/server';
|
import type { CloudSetup } from '@kbn/cloud-plugin/server';
|
||||||
|
@ -34,11 +34,14 @@ describe('Spaces plugin', () => {
|
||||||
expect(spacesSetup).toMatchInlineSnapshot(`
|
expect(spacesSetup).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"hasOnlyDefaultSpace$": Observable {
|
"hasOnlyDefaultSpace$": Observable {
|
||||||
|
"operator": [Function],
|
||||||
|
"source": Observable {
|
||||||
"operator": [Function],
|
"operator": [Function],
|
||||||
"source": Observable {
|
"source": Observable {
|
||||||
"_subscribe": [Function],
|
"_subscribe": [Function],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
"spacesClient": Object {
|
"spacesClient": Object {
|
||||||
"registerClientWrapper": [Function],
|
"registerClientWrapper": [Function],
|
||||||
"setClientRepositoryFactory": [Function],
|
"setClientRepositoryFactory": [Function],
|
||||||
|
@ -118,11 +121,14 @@ describe('Spaces plugin', () => {
|
||||||
expect(spacesStart).toMatchInlineSnapshot(`
|
expect(spacesStart).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"hasOnlyDefaultSpace$": Observable {
|
"hasOnlyDefaultSpace$": Observable {
|
||||||
|
"operator": [Function],
|
||||||
|
"source": Observable {
|
||||||
"operator": [Function],
|
"operator": [Function],
|
||||||
"source": Observable {
|
"source": Observable {
|
||||||
"_subscribe": [Function],
|
"_subscribe": [Function],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
"spacesService": Object {
|
"spacesService": Object {
|
||||||
"createSpacesClient": [Function],
|
"createSpacesClient": [Function],
|
||||||
"getActiveSpace": [Function],
|
"getActiveSpace": [Function],
|
||||||
|
@ -150,8 +156,8 @@ describe('Spaces plugin', () => {
|
||||||
const coreStart = coreMock.createStart();
|
const coreStart = coreMock.createStart();
|
||||||
const spacesStart = plugin.start(coreStart);
|
const spacesStart = plugin.start(coreStart);
|
||||||
|
|
||||||
await expect(lastValueFrom(spacesSetup.hasOnlyDefaultSpace$)).resolves.toEqual(true);
|
await expect(firstValueFrom(spacesSetup.hasOnlyDefaultSpace$)).resolves.toEqual(true);
|
||||||
await expect(lastValueFrom(spacesStart.hasOnlyDefaultSpace$)).resolves.toEqual(true);
|
await expect(firstValueFrom(spacesStart.hasOnlyDefaultSpace$)).resolves.toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('determines hasOnlyDefaultSpace$ correctly when maxSpaces=1000', async () => {
|
it('determines hasOnlyDefaultSpace$ correctly when maxSpaces=1000', async () => {
|
||||||
|
@ -168,7 +174,7 @@ describe('Spaces plugin', () => {
|
||||||
const coreStart = coreMock.createStart();
|
const coreStart = coreMock.createStart();
|
||||||
const spacesStart = plugin.start(coreStart);
|
const spacesStart = plugin.start(coreStart);
|
||||||
|
|
||||||
await expect(lastValueFrom(spacesSetup.hasOnlyDefaultSpace$)).resolves.toEqual(false);
|
await expect(firstValueFrom(spacesSetup.hasOnlyDefaultSpace$)).resolves.toEqual(false);
|
||||||
await expect(lastValueFrom(spacesStart.hasOnlyDefaultSpace$)).resolves.toEqual(false);
|
await expect(firstValueFrom(spacesStart.hasOnlyDefaultSpace$)).resolves.toEqual(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Observable } from 'rxjs';
|
import type { Observable } from 'rxjs';
|
||||||
import { map } from 'rxjs';
|
import { BehaviorSubject, combineLatest, map } from 'rxjs';
|
||||||
|
|
||||||
import type { CloudSetup } from '@kbn/cloud-plugin/server';
|
import type { CloudSetup } from '@kbn/cloud-plugin/server';
|
||||||
import type {
|
import type {
|
||||||
|
@ -119,8 +119,21 @@ export class SpacesPlugin
|
||||||
|
|
||||||
private defaultSpaceService?: DefaultSpaceService;
|
private defaultSpaceService?: DefaultSpaceService;
|
||||||
|
|
||||||
|
private onCloud$ = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||||
this.config$ = initializerContext.config.create<ConfigType>();
|
this.config$ = combineLatest([
|
||||||
|
initializerContext.config.create<ConfigType>(),
|
||||||
|
this.onCloud$,
|
||||||
|
]).pipe(
|
||||||
|
map(
|
||||||
|
([config, onCloud]): ConfigType => ({
|
||||||
|
...config,
|
||||||
|
// We only allow "solution" to be set on cloud environments, not on prem
|
||||||
|
allowSolutionVisibility: onCloud ? config.allowSolutionVisibility : false,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
this.hasOnlyDefaultSpace$ = this.config$.pipe(map(({ maxSpaces }) => maxSpaces === 1));
|
this.hasOnlyDefaultSpace$ = this.config$.pipe(map(({ maxSpaces }) => maxSpaces === 1));
|
||||||
this.log = initializerContext.logger.get();
|
this.log = initializerContext.logger.get();
|
||||||
this.spacesService = new SpacesService();
|
this.spacesService = new SpacesService();
|
||||||
|
@ -131,6 +144,7 @@ export class SpacesPlugin
|
||||||
}
|
}
|
||||||
|
|
||||||
public setup(core: CoreSetup<PluginsStart>, plugins: PluginsSetup): SpacesPluginSetup {
|
public setup(core: CoreSetup<PluginsStart>, plugins: PluginsSetup): SpacesPluginSetup {
|
||||||
|
this.onCloud$.next(plugins.cloud !== undefined && plugins.cloud.isCloudEnabled);
|
||||||
const spacesClientSetup = this.spacesClientService.setup({ config$: this.config$ });
|
const spacesClientSetup = this.spacesClientService.setup({ config$: this.config$ });
|
||||||
|
|
||||||
const spacesServiceSetup = this.spacesService.setup({
|
const spacesServiceSetup = this.spacesService.setup({
|
||||||
|
|
|
@ -22,6 +22,7 @@ const createMockConfig = (
|
||||||
enabled: true,
|
enabled: true,
|
||||||
maxSpaces: 1000,
|
maxSpaces: 1000,
|
||||||
allowFeatureVisibility: true,
|
allowFeatureVisibility: true,
|
||||||
|
allowSolutionVisibility: true,
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
return ConfigSchema.validate(mockConfig, { serverless: !mockConfig.allowFeatureVisibility });
|
return ConfigSchema.validate(mockConfig, { serverless: !mockConfig.allowFeatureVisibility });
|
||||||
|
@ -311,6 +312,7 @@ describe('#create', () => {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
maxSpaces,
|
maxSpaces,
|
||||||
allowFeatureVisibility: true,
|
allowFeatureVisibility: true,
|
||||||
|
allowSolutionVisibility: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const client = new SpacesClient(
|
const client = new SpacesClient(
|
||||||
|
@ -347,6 +349,7 @@ describe('#create', () => {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
maxSpaces,
|
maxSpaces,
|
||||||
allowFeatureVisibility: true,
|
allowFeatureVisibility: true,
|
||||||
|
allowSolutionVisibility: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const client = new SpacesClient(
|
const client = new SpacesClient(
|
||||||
|
@ -382,6 +385,7 @@ describe('#create', () => {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
maxSpaces,
|
maxSpaces,
|
||||||
allowFeatureVisibility: true,
|
allowFeatureVisibility: true,
|
||||||
|
allowSolutionVisibility: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const client = new SpacesClient(
|
const client = new SpacesClient(
|
||||||
|
@ -428,6 +432,7 @@ describe('#create', () => {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
maxSpaces,
|
maxSpaces,
|
||||||
allowFeatureVisibility: true,
|
allowFeatureVisibility: true,
|
||||||
|
allowSolutionVisibility: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const client = new SpacesClient(
|
const client = new SpacesClient(
|
||||||
|
@ -470,6 +475,7 @@ describe('#create', () => {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
maxSpaces,
|
maxSpaces,
|
||||||
allowFeatureVisibility: false,
|
allowFeatureVisibility: false,
|
||||||
|
allowSolutionVisibility: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const client = new SpacesClient(
|
const client = new SpacesClient(
|
||||||
|
@ -506,6 +512,7 @@ describe('#create', () => {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
maxSpaces,
|
maxSpaces,
|
||||||
allowFeatureVisibility: false,
|
allowFeatureVisibility: false,
|
||||||
|
allowSolutionVisibility: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const client = new SpacesClient(
|
const client = new SpacesClient(
|
||||||
|
@ -530,6 +537,46 @@ describe('#create', () => {
|
||||||
expect(mockCallWithRequestRepository.create).not.toHaveBeenCalled();
|
expect(mockCallWithRequestRepository.create).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when config.allowSolutionVisibility is disabled', () => {
|
||||||
|
test(`throws bad request when creating space with solution`, async () => {
|
||||||
|
const maxSpaces = 5;
|
||||||
|
const mockDebugLogger = createMockDebugLogger();
|
||||||
|
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
|
||||||
|
mockCallWithRequestRepository.create.mockResolvedValue(savedObject);
|
||||||
|
mockCallWithRequestRepository.find.mockResolvedValue({
|
||||||
|
total: maxSpaces - 1,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const mockConfig = createMockConfig({
|
||||||
|
enabled: true,
|
||||||
|
maxSpaces,
|
||||||
|
allowFeatureVisibility: false,
|
||||||
|
allowSolutionVisibility: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = new SpacesClient(
|
||||||
|
mockDebugLogger,
|
||||||
|
mockConfig,
|
||||||
|
mockCallWithRequestRepository,
|
||||||
|
[],
|
||||||
|
'traditional'
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
client.create({ ...spaceToCreate, solution: 'es' })
|
||||||
|
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Unable to create Space, the solution property can not be set when xpack.spaces.allowSolutionVisibility setting is disabled"`
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockCallWithRequestRepository.find).toHaveBeenCalledWith({
|
||||||
|
type: 'space',
|
||||||
|
page: 1,
|
||||||
|
perPage: 0,
|
||||||
|
});
|
||||||
|
expect(mockCallWithRequestRepository.create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#update', () => {
|
describe('#update', () => {
|
||||||
|
@ -675,6 +722,7 @@ describe('#update', () => {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
maxSpaces: 1000,
|
maxSpaces: 1000,
|
||||||
allowFeatureVisibility: false,
|
allowFeatureVisibility: false,
|
||||||
|
allowSolutionVisibility: false,
|
||||||
});
|
});
|
||||||
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
|
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
|
||||||
mockCallWithRequestRepository.get.mockResolvedValue(savedObject);
|
mockCallWithRequestRepository.get.mockResolvedValue(savedObject);
|
||||||
|
@ -700,6 +748,7 @@ describe('#update', () => {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
maxSpaces: 1000,
|
maxSpaces: 1000,
|
||||||
allowFeatureVisibility: false,
|
allowFeatureVisibility: false,
|
||||||
|
allowSolutionVisibility: false,
|
||||||
});
|
});
|
||||||
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
|
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
|
||||||
mockCallWithRequestRepository.get.mockResolvedValue(savedObject);
|
mockCallWithRequestRepository.get.mockResolvedValue(savedObject);
|
||||||
|
@ -723,6 +772,38 @@ describe('#update', () => {
|
||||||
expect(mockCallWithRequestRepository.get).not.toHaveBeenCalled();
|
expect(mockCallWithRequestRepository.get).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when config.allowSolutionVisibility is disabled', () => {
|
||||||
|
test(`throws bad request when updating space with solution`, async () => {
|
||||||
|
const mockDebugLogger = createMockDebugLogger();
|
||||||
|
const mockConfig = createMockConfig({
|
||||||
|
enabled: true,
|
||||||
|
maxSpaces: 1000,
|
||||||
|
allowFeatureVisibility: false,
|
||||||
|
allowSolutionVisibility: false,
|
||||||
|
});
|
||||||
|
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
|
||||||
|
mockCallWithRequestRepository.get.mockResolvedValue(savedObject);
|
||||||
|
|
||||||
|
const client = new SpacesClient(
|
||||||
|
mockDebugLogger,
|
||||||
|
mockConfig,
|
||||||
|
mockCallWithRequestRepository,
|
||||||
|
[],
|
||||||
|
'traditional'
|
||||||
|
);
|
||||||
|
const id = savedObject.id;
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
client.update(id, { ...spaceToUpdate, solution: 'es' })
|
||||||
|
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Unable to update Space, the solution property can not be set when xpack.spaces.allowSolutionVisibility setting is disabled"`
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockCallWithRequestRepository.update).not.toHaveBeenCalled();
|
||||||
|
expect(mockCallWithRequestRepository.get).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#delete', () => {
|
describe('#delete', () => {
|
||||||
|
|
|
@ -136,6 +136,12 @@ export class SpacesClient implements ISpacesClient {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Boolean(space.solution) && !this.config.allowSolutionVisibility) {
|
||||||
|
throw Boom.badRequest(
|
||||||
|
'Unable to create Space, the solution property can not be set when xpack.spaces.allowSolutionVisibility setting is disabled'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isServerless && space.hasOwnProperty('solution')) {
|
if (this.isServerless && space.hasOwnProperty('solution')) {
|
||||||
throw Boom.badRequest('Unable to create Space, solution property is forbidden in serverless');
|
throw Boom.badRequest('Unable to create Space, solution property is forbidden in serverless');
|
||||||
}
|
}
|
||||||
|
@ -163,6 +169,12 @@ export class SpacesClient implements ISpacesClient {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Boolean(space.solution) && !this.config.allowSolutionVisibility) {
|
||||||
|
throw Boom.badRequest(
|
||||||
|
'Unable to update Space, the solution property can not be set when xpack.spaces.allowSolutionVisibility setting is disabled'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isServerless && space.hasOwnProperty('solution')) {
|
if (this.isServerless && space.hasOwnProperty('solution')) {
|
||||||
throw Boom.badRequest('Unable to update Space, solution property is forbidden in serverless');
|
throw Boom.badRequest('Unable to update Space, solution property is forbidden in serverless');
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,6 @@
|
||||||
"@kbn/utility-types-jest",
|
"@kbn/utility-types-jest",
|
||||||
"@kbn/security-plugin-types-public",
|
"@kbn/security-plugin-types-public",
|
||||||
"@kbn/cloud-plugin",
|
"@kbn/cloud-plugin",
|
||||||
"@kbn/cloud-experiments-plugin",
|
|
||||||
"@kbn/core-analytics-browser"
|
"@kbn/core-analytics-browser"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
|
|
|
@ -7,6 +7,11 @@
|
||||||
|
|
||||||
import { FtrConfigProviderContext } from '@kbn/test';
|
import { FtrConfigProviderContext } from '@kbn/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOTE: The solution view is currently only available in the cloud environment.
|
||||||
|
* This test suite fakes a cloud environement by setting the cloud.id and cloud.base_url
|
||||||
|
*/
|
||||||
|
|
||||||
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
||||||
const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js'));
|
const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js'));
|
||||||
|
|
||||||
|
@ -17,10 +22,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
||||||
...functionalConfig.get('kbnTestServer'),
|
...functionalConfig.get('kbnTestServer'),
|
||||||
serverArgs: [
|
serverArgs: [
|
||||||
...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',
|
|
||||||
// Note: the base64 string in the cloud.id config contains the ES endpoint required in the functional tests
|
// 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.id=ftr_fake_cloud_id:aGVsbG8uY29tOjQ0MyRFUzEyM2FiYyRrYm4xMjNhYmM=',
|
||||||
'--xpack.cloud.base_url=https://cloud.elastic.co',
|
'--xpack.cloud.base_url=https://cloud.elastic.co',
|
||||||
|
|
|
@ -70,6 +70,10 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
|
||||||
...disabledPlugins
|
...disabledPlugins
|
||||||
.filter((k) => k !== 'security')
|
.filter((k) => k !== 'security')
|
||||||
.map((key) => `--xpack.${key}.enabled=false`),
|
.map((key) => `--xpack.${key}.enabled=false`),
|
||||||
|
// Note: we fake a cloud deployment as the solution view is only available in cloud
|
||||||
|
'--xpack.cloud.id=ftr_fake_cloud_id:aGVsbG8uY29tOjQ0MyRFUzEyM2FiYyRrYm4xMjNhYmM=',
|
||||||
|
'--xpack.cloud.base_url=https://cloud.elastic.co',
|
||||||
|
'--xpack.cloud.deployment_url=/deployments/deploymentId',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue