[settings] Extract and fix Section Registry (#163502)

## Summary

While working to extract various portions of the `advancedSettings`
plugin into packages, I found the `ComponentRegistry` in the plugin to
have a number of issues that contributed to a fairly bad UX:

- the API allows for adding/overriding the title, subtitle and footer of
the Advanced Settings page, but only the footer is rendered.
- the API is available to all plugins, but only renders a single
entry... so depending on the plugin load order, the render is not
guaranteed.
- filtering the footer in or out of the display is delegated to the
component itself, so:
  - it only takes effect on render.
- the count is only updated if you click on the page that contains it,
but that logic is currently broken.
  - the error message is inaccurate.

![Aug-09-2023
11-19-06](494aba14-f2c0-4ce7-b3f0-1910824aeb0e)

This PR fixes those issues and more:

- extracts the registry into its own package.
- changes the API to allow for multiple sections from multiple plugins.
- changes the API to filter these sections from the plugin, rather than
from each individual component.
- fixes state management to show sections, keep counts accurate, etc.

![Aug-09-2023
11-02-11](d8e8033c-f9ed-4615-b954-b5c23fda4d7a)

---------

Co-authored-by: Vadim Kibana <82822460+vadimkibana@users.noreply.github.com>
This commit is contained in:
Clint Andrew Hall 2023-08-14 14:00:33 -04:00 committed by GitHub
parent 309666acc2
commit 1546490e98
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 433 additions and 807 deletions

3
.github/CODEOWNERS vendored
View file

@ -9,7 +9,7 @@ x-pack/test/alerting_api_integration/common/plugins/aad @elastic/response-ops
packages/kbn-ace @elastic/platform-deployment-management
x-pack/plugins/actions @elastic/response-ops
x-pack/test/alerting_api_integration/common/plugins/actions_simulators @elastic/response-ops
src/plugins/advanced_settings @elastic/appex-sharedux
src/plugins/advanced_settings @elastic/appex-sharedux @elastic/platform-deployment-management
x-pack/packages/ml/aiops_components @elastic/ml-ui
x-pack/plugins/aiops @elastic/ml-ui
x-pack/packages/ml/aiops_utils @elastic/ml-ui
@ -478,6 +478,7 @@ packages/kbn-managed-vscode-config @elastic/kibana-operations
packages/kbn-managed-vscode-config-cli @elastic/kibana-operations
packages/kbn-management/cards_navigation @elastic/platform-deployment-management
src/plugins/management @elastic/platform-deployment-management
packages/kbn-management/settings/section_registry @elastic/appex-sharedux @elastic/platform-deployment-management
packages/kbn-management/storybook/config @elastic/platform-deployment-management
test/plugin_functional/plugins/management_test_plugin @elastic/kibana-app-services
packages/kbn-mapbox-gl @elastic/kibana-gis

View file

@ -495,6 +495,7 @@
"@kbn/logstash-plugin": "link:x-pack/plugins/logstash",
"@kbn/management-cards-navigation": "link:packages/kbn-management/cards_navigation",
"@kbn/management-plugin": "link:src/plugins/management",
"@kbn/management-settings-section-registry": "link:packages/kbn-management/settings/section_registry",
"@kbn/management-test-plugin": "link:test/plugin_functional/plugins/management_test_plugin",
"@kbn/mapbox-gl": "link:packages/kbn-mapbox-gl",
"@kbn/maps-custom-raster-source-plugin": "link:x-pack/examples/third_party_maps_source_example",

View file

@ -0,0 +1,56 @@
---
id: kbn-management/settings/SectionRegistry
slug: /kbn-management/settings/section-registry/
title: Section Registry
description: A registry which allows a consumer to add sections to Advanced Settings.
tags: ['management', 'settings']
date: 2023-08-04
---
This registry is fairly straightforward: it allows a consumer to add a section to the Advanced Settings page. This registry would be consumed by a plugin and exposed on the `start` and `setup` contracts:
```ts
const registry = new SectionRegistry();
export class PluginBar
implements Plugin<PluginBarSetup, PluginBarStart, PluginBarSetupDeps, PluginBarStartDeps>
{
public setup(
_core: CoreSetup,
_setupDeps: PluginFooSetupDeps
) {
return {
sectionRegistry: sectionRegistry.setup,
};
}
public start(
_core: CoreStart,
_startDeps: PluginFooStartDeps
) {
return {
sectionRegistry: sectionRegistry.start,
};
}
}
export class PluginFoo
implements Plugin<PluginFooSetup, PluginFooStart, PluginFooSetupDeps, PluginFooStartDeps>
{
public setup(
core: CoreSetup,
{ pluginBar: { sectionRegistry } }: PluginFooSetupDeps
) {
const Component = (props: RegistryComponentProps) => <SomeComponent {...props} />;
const queryMatch = (query: string) => {
const searchTerm = query.toLowerCase();
return SEARCH_TERMS.some((term) => term.indexOf(searchTerm) >= 0);
};
sectionRegistry.setup.addGlobalSection(Component, queryMatch);
}
}
```

View file

@ -6,4 +6,10 @@
* Side Public License, v 1.
*/
export { PageSubtitle } from './page_subtitle';
export { SectionRegistry } from './section_registry';
export type {
SectionRegistrySetup,
SectionRegistryStart,
RegistryComponentProps,
RegistryEntry,
} from './section_registry';

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../../..',
roots: ['<rootDir>/packages/kbn-management/settings/section_registry'],
coverageDirectory:
'<rootDir>/target/kibana-coverage/jest/packages/kbn-management/settings/section_registry',
coverageReporters: ['text', 'html'],
collectCoverageFrom: [
'<rootDir>/packages/kbn-management/settings/section_registry/**/*.{ts,tsx}',
],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/management-settings-section-registry",
"owner": "@elastic/appex-sharedux @elastic/platform-deployment-management"
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/management-settings-section-registry",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { SectionRegistry } from './section_registry';
describe('SectionRegistry', () => {
let registry = new SectionRegistry();
beforeEach(() => {
registry = new SectionRegistry();
});
describe('register', () => {
it('should allow a global component to be registered', () => {
const Component = () => <div />;
const queryMatch = () => true;
registry.setup.addGlobalSection(Component, queryMatch);
const entries = registry.start.getGlobalSections();
expect(entries).toHaveLength(1);
expect(entries[0].Component).toBe(Component);
expect(entries[0].queryMatch).toBe(queryMatch);
expect(registry.start.getSpacesSections()).toHaveLength(0);
});
it('should allow a spaces component to be registered', () => {
const Component = () => <div />;
const queryMatch = () => true;
registry.setup.addSpaceSection(Component, queryMatch);
const entries = registry.start.getSpacesSections();
expect(entries).toHaveLength(1);
expect(entries[0].Component).toBe(Component);
expect(entries[0].queryMatch).toBe(queryMatch);
expect(registry.start.getGlobalSections()).toHaveLength(0);
});
});
});

View file

@ -0,0 +1,90 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { ComponentType } from 'react';
import { ToastsStart } from '@kbn/core-notifications-browser';
import { UiSettingsScope } from '@kbn/core-ui-settings-common';
/**
* Props provided to a `RegistryComponent`.
*/
export interface RegistryComponentProps {
toasts: ToastsStart;
enableSaving: Record<UiSettingsScope, boolean>;
}
/**
* A registry entry for a section.
*/
export interface RegistryEntry {
Component: RegistryComponent;
queryMatch: QueryMatchFn;
}
type RegistryComponent = ComponentType<RegistryComponentProps>;
type PageType = 'space' | 'global';
type QueryMatchFn = (term: string) => boolean;
type Registry = Record<PageType, RegistryEntry[]>;
/**
* A registry of sections to add to pages within Advanced Settings.
*/
export class SectionRegistry {
private registry: Registry = {
space: [],
global: [],
};
setup = {
/**
* Registers a section within the "Space" page.
*
* @param Component - A React component to render.
* @param queryMatch - A function that, given a search term, returns true if the section should be rendered.
*/
addSpaceSection: (Component: RegistryComponent, queryMatch: QueryMatchFn) => {
this.registry.space.push({ Component, queryMatch });
},
/**
* Registers a section within the "Global" page.
*
* @param Component - A React component to render.
* @param queryMatch - A function that, given a search term, returns true if the section should be rendered.
*/
addGlobalSection: (Component: RegistryComponent, queryMatch: QueryMatchFn) => {
this.registry.global.push({ Component, queryMatch });
},
};
start = {
/**
* Retrieve components registered for the "Space" page.
*/
getGlobalSections: (): RegistryEntry[] => {
return this.registry.global;
},
/**
* Retrieve components registered for the "Global" page.
*/
getSpacesSections: (): RegistryEntry[] => {
return this.registry.space;
},
};
}
/**
* The `setup` contract provided by a `SectionRegistry`.
*/
export type SectionRegistrySetup = SectionRegistry['setup'];
/**
* The `start` contract provided by a `SectionRegistry`.
*/
export type SectionRegistryStart = SectionRegistry['start'];

View file

@ -0,0 +1,22 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react"
]
},
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/core-notifications-browser",
"@kbn/core-ui-settings-common",
]
}

View file

@ -1,7 +1,7 @@
{
"type": "plugin",
"id": "@kbn/advanced-settings-plugin",
"owner": "@elastic/appex-sharedux",
"owner": "@elastic/appex-sharedux @elastic/platform-deployment-management",
"plugin": {
"id": "advancedSettings",
"server": true,
@ -18,4 +18,4 @@
"kibanaUtils"
]
}
}
}

View file

@ -1,3 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ComponentRegistry register should disallow registering a component with a duplicate id 1`] = `"Component with id advanced_settings_page_title is already registered."`;

View file

@ -1,79 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { ComponentRegistry } from './component_registry';
describe('ComponentRegistry', () => {
describe('register', () => {
it('should allow a component to be registered', () => {
const component = () => <div />;
new ComponentRegistry().setup.register(
ComponentRegistry.componentType.PAGE_TITLE_COMPONENT,
component
);
});
it('should disallow registering a component with a duplicate id', () => {
const registry = new ComponentRegistry();
const component = () => <div />;
registry.setup.register(ComponentRegistry.componentType.PAGE_TITLE_COMPONENT, component);
expect(() =>
registry.setup.register(ComponentRegistry.componentType.PAGE_TITLE_COMPONENT, () => (
<span />
))
).toThrowErrorMatchingSnapshot();
});
it('should allow a component to be overriden', () => {
const registry = new ComponentRegistry();
const component = () => <div />;
registry.setup.register(ComponentRegistry.componentType.PAGE_TITLE_COMPONENT, component);
const anotherComponent = () => <span />;
registry.setup.register(
ComponentRegistry.componentType.PAGE_TITLE_COMPONENT,
anotherComponent,
true
);
expect(registry.start.get(ComponentRegistry.componentType.PAGE_TITLE_COMPONENT)).toBe(
anotherComponent
);
});
});
describe('get', () => {
it('should allow a component to be retrieved', () => {
const registry = new ComponentRegistry();
const component = () => <div />;
registry.setup.register(ComponentRegistry.componentType.PAGE_TITLE_COMPONENT, component);
expect(registry.start.get(ComponentRegistry.componentType.PAGE_TITLE_COMPONENT)).toBe(
component
);
});
});
it('should set a displayName for the component if one does not exist', () => {
const component: React.ComponentType = () => <div />;
const registry = new ComponentRegistry();
registry.setup.register(ComponentRegistry.componentType.PAGE_TITLE_COMPONENT, component);
expect(component.displayName).toEqual(ComponentRegistry.componentType.PAGE_TITLE_COMPONENT);
});
it('should not set a displayName for the component if one already exists', () => {
const component: React.ComponentType = () => <div />;
component.displayName = '<AwesomeComponent>';
const registry = new ComponentRegistry();
registry.setup.register(ComponentRegistry.componentType.PAGE_TITLE_COMPONENT, component);
expect(component.displayName).toEqual('<AwesomeComponent>');
});
});

View file

@ -1,76 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ComponentType } from 'react';
import { PageTitle } from './page_title';
import { PageSubtitle } from './page_subtitle';
import { PageFooter } from './page_footer';
type Id =
| 'advanced_settings_page_title'
| 'advanced_settings_page_subtitle'
| 'advanced_settings_page_footer';
const componentType: { [key: string]: Id } = {
PAGE_TITLE_COMPONENT: 'advanced_settings_page_title' as Id,
PAGE_SUBTITLE_COMPONENT: 'advanced_settings_page_subtitle' as Id,
PAGE_FOOTER_COMPONENT: 'advanced_settings_page_footer' as Id,
};
type RegistryComponent = ComponentType<Record<string, any> | undefined>;
export class ComponentRegistry {
static readonly componentType = componentType;
static readonly defaultRegistry: Record<Id, RegistryComponent> = {
advanced_settings_page_title: PageTitle,
advanced_settings_page_subtitle: PageSubtitle,
advanced_settings_page_footer: PageFooter,
};
registry: { [key in Id]?: RegistryComponent } = {};
setup = {
componentType: ComponentRegistry.componentType,
/**
* Attempts to register the provided component, with the ability to optionally allow
* the component to override an existing one.
*
* If the intent is to override, then `allowOverride` must be set to true, otherwise an exception is thrown.
*
* @param id the id of the component to register
* @param component the component
* @param allowOverride (default: false) - optional flag to allow this component to override a previously registered component
*/
register: (id: Id, component: RegistryComponent, allowOverride = false) => {
if (!allowOverride && id in this.registry) {
throw new Error(`Component with id ${id} is already registered.`);
}
// Setting a display name if one does not already exist.
// This enhances the snapshots, as well as the debugging experience.
if (!component.displayName) {
component.displayName = id;
}
this.registry[id] = component;
},
};
start = {
componentType: ComponentRegistry.componentType,
/**
* Retrieve a registered component by its ID.
* If the component does not exist, then an exception is thrown.
*
* @param id the ID of the component to retrieve
*/
get: (id: Id): RegistryComponent => {
return this.registry[id] || ComponentRegistry.defaultRegistry[id];
},
};
}

View file

@ -1,9 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { ComponentRegistry } from './component_registry';

View file

@ -1,3 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PageFooter should render normally 1`] = `""`;

View file

@ -1,18 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { shallowWithI18nProvider } from '@kbn/test-jest-helpers';
import { PageFooter } from './page_footer';
describe('PageFooter', () => {
it('should render normally', () => {
expect(shallowWithI18nProvider(<PageFooter />)).toMatchSnapshot();
});
});

View file

@ -1,9 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const PageFooter = () => null;

View file

@ -1,3 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PageSubtitle should render normally 1`] = `""`;

View file

@ -1,18 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { shallowWithI18nProvider } from '@kbn/test-jest-helpers';
import { PageSubtitle } from './page_subtitle';
describe('PageSubtitle', () => {
it('should render normally', () => {
expect(shallowWithI18nProvider(<PageSubtitle />)).toMatchSnapshot();
});
});

View file

@ -1,9 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const PageSubtitle = () => null;

View file

@ -1,15 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PageTitle should render normally 1`] = `
<EuiText>
<h1
data-test-subj="managementSettingsTitle"
>
<FormattedMessage
defaultMessage="Settings"
id="advancedSettings.pageTitle"
values={Object {}}
/>
</h1>
</EuiText>
`;

View file

@ -1,9 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { PageTitle } from './page_title';

View file

@ -1,18 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { shallowWithI18nProvider } from '@kbn/test-jest-helpers';
import { PageTitle } from './page_title';
describe('PageTitle', () => {
it('should render normally', () => {
expect(shallowWithI18nProvider(<PageTitle />)).toMatchSnapshot();
});
});

View file

@ -1,21 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
export const PageTitle = () => {
return (
<EuiText>
<h1 data-test-subj="managementSettingsTitle">
<FormattedMessage id="advancedSettings.pageTitle" defaultMessage="Settings" />
</h1>
</EuiText>
);
};

View file

@ -10,7 +10,6 @@ import React from 'react';
import { PluginInitializerContext } from '@kbn/core/public';
import { AdvancedSettingsPlugin } from './plugin';
export type { AdvancedSettingsSetup, AdvancedSettingsStart } from './types';
export { ComponentRegistry } from './component_registry';
/**
* Exports the field component as a React.lazy component. We're explicitly naming it lazy here

View file

@ -13,7 +13,7 @@ export const i18nTexts = {
defaultMessage: 'Space Settings',
}),
defaultSpaceCalloutTitle: i18n.translate('advancedSettings.defaultSpaceCalloutTitle', {
defaultMessage: 'Changes will affect the `default` space',
defaultMessage: 'Changes will affect the current space.',
}),
defaultSpaceCalloutSubtitle: i18n.translate('advancedSettings.defaultSpaceCalloutSubtitle', {
defaultMessage:

View file

@ -12,18 +12,17 @@ import { Redirect, RouteChildrenProps } from 'react-router-dom';
import { Router, Routes, Route } from '@kbn/shared-ux-router';
import { i18n } from '@kbn/i18n';
import { I18nProvider } from '@kbn/i18n-react';
import { LocationDescriptor } from 'history';
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { url } from '@kbn/kibana-utils-plugin/public';
import { ManagementAppMountParams } from '@kbn/management-plugin/public';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import { StartServicesAccessor } from '@kbn/core/public';
import type { SectionRegistryStart } from '@kbn/management-settings-section-registry';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { QUERY } from './advanced_settings';
import { Settings } from './settings';
import { ComponentRegistry } from '../types';
import './index.scss';
@ -56,11 +55,12 @@ const redirectUrl = ({ match, location }: RedirectUrlProps): LocationDescriptor
export async function mountManagementSection(
getStartServices: StartServicesAccessor,
params: ManagementAppMountParams,
componentRegistry: ComponentRegistry['start'],
sectionRegistry: SectionRegistryStart,
usageCollection?: UsageCollectionSetup
) {
params.setBreadcrumbs(crumb);
const [{ settings, notifications, docLinks, application, chrome }] = await getStartServices();
const [{ settings, notifications, docLinks, application, chrome, i18n: i18nStart, theme }] =
await getStartServices();
const { advancedSettings, globalSettings } = application.capabilities;
const canSaveAdvancedSettings = advancedSettings.save as boolean;
@ -74,34 +74,32 @@ export async function mountManagementSection(
chrome.docTitle.change(title);
ReactDOM.render(
<KibanaThemeProvider theme$={params.theme$}>
<I18nProvider>
<Router history={params.history}>
<Routes>
{/* TODO: remove route param (`query`) in 7.13 */}
<Route path={`/:${QUERY}`}>
{(props: RedirectUrlProps) => <Redirect to={redirectUrl(props)} />}
</Route>
<Route path="/">
<Settings
history={params.history}
enableSaving={{
namespace: canSaveAdvancedSettings,
global: canSaveGlobalSettings,
}}
enableShowing={{ namespace: true, global: canShowGlobalSettings }}
toasts={notifications.toasts}
docLinks={docLinks.links}
settingsService={settings}
theme={params.theme$}
componentRegistry={componentRegistry}
trackUiMetric={trackUiMetric}
/>
</Route>
</Routes>
</Router>
</I18nProvider>
</KibanaThemeProvider>,
<KibanaRenderContextProvider {...{ i18n: i18nStart, theme }}>
<Router history={params.history}>
<Routes>
{/* TODO: remove route param (`query`) in 7.13 */}
<Route path={`/:${QUERY}`}>
{(props: RedirectUrlProps) => <Redirect to={redirectUrl(props)} />}
</Route>
<Route path="/">
<Settings
history={params.history}
enableSaving={{
namespace: canSaveAdvancedSettings,
global: canSaveGlobalSettings,
}}
enableShowing={{ namespace: true, global: canShowGlobalSettings }}
toasts={notifications.toasts}
docLinks={docLinks.links}
settingsService={settings}
theme={params.theme$}
sectionRegistry={sectionRegistry}
trackUiMetric={trackUiMetric}
/>
</Route>
</Routes>
</Router>
</KibanaRenderContextProvider>,
params.element
);
return () => {

View file

@ -21,7 +21,7 @@ import {
docLinksServiceMock,
themeServiceMock,
} from '@kbn/core/public/mocks';
import { ComponentRegistry } from '../component_registry';
import { SectionRegistry } from '@kbn/management-settings-section-registry';
import { Search } from './components/search';
import { EuiTab } from '@elastic/eui';
@ -257,7 +257,7 @@ describe('Settings', () => {
toasts={notificationServiceMock.createStartContract().toasts}
docLinks={docLinksServiceMock.createStartContract().links}
settingsService={mockConfig().core.settings}
componentRegistry={new ComponentRegistry().start}
sectionRegistry={new SectionRegistry().start}
theme={themeServiceMock.createStartContract().theme$}
/>
);
@ -286,7 +286,7 @@ describe('Settings', () => {
toasts={notificationServiceMock.createStartContract().toasts}
docLinks={docLinksServiceMock.createStartContract().links}
settingsService={mockConfig().core.settings}
componentRegistry={new ComponentRegistry().start}
sectionRegistry={new SectionRegistry().start}
theme={themeServiceMock.createStartContract().theme$}
/>
);
@ -312,7 +312,7 @@ describe('Settings', () => {
toasts={notificationServiceMock.createStartContract().toasts}
docLinks={docLinksServiceMock.createStartContract().links}
settingsService={mockConfig().core.settings}
componentRegistry={new ComponentRegistry().start}
sectionRegistry={new SectionRegistry().start}
theme={themeServiceMock.createStartContract().theme$}
/>
);
@ -341,7 +341,7 @@ describe('Settings', () => {
toasts={toasts}
docLinks={docLinksServiceMock.createStartContract().links}
settingsService={mockConfig().core.settings}
componentRegistry={new ComponentRegistry().start}
sectionRegistry={new SectionRegistry().start}
theme={themeServiceMock.createStartContract().theme$}
/>
);
@ -363,7 +363,7 @@ describe('Settings', () => {
toasts={toasts}
docLinks={docLinksServiceMock.createStartContract().links}
settingsService={mockConfig().core.settings}
componentRegistry={new ComponentRegistry().start}
sectionRegistry={new SectionRegistry().start}
theme={themeServiceMock.createStartContract().theme$}
/>
);

View file

@ -26,19 +26,23 @@ import { UiCounterMetricType } from '@kbn/analytics';
import { url } from '@kbn/kibana-utils-plugin/common';
import { parse } from 'query-string';
import { UiSettingsScope } from '@kbn/core-ui-settings-common';
import type { SectionRegistryStart } from '@kbn/management-settings-section-registry';
import type { RegistryEntry } from '@kbn/management-settings-section-registry';
import { mapConfig, mapSettings, initCategoryCounts, initCategories } from './settings_helper';
import { parseErrorMsg } from './components/search/search';
import { AdvancedSettings, QUERY } from './advanced_settings';
import { ComponentRegistry } from '..';
import { Search } from './components/search';
import { FieldSetting } from './types';
import { i18nTexts } from './i18n_texts';
import { getAriaName } from './lib';
interface AdvancedSettingsState {
footerQueryMatched: boolean;
query: Query;
filteredSettings: Record<UiSettingsScope, Record<string, FieldSetting[]>>;
filteredSections: {
global: RegistryEntry[];
space: RegistryEntry[];
};
}
export type GroupedSettings = Record<string, FieldSetting[]>;
@ -51,7 +55,7 @@ interface Props {
docLinks: DocLinksStart['links'];
toasts: ToastsStart;
theme: ThemeServiceStart['theme$'];
componentRegistry: ComponentRegistry['start'];
sectionRegistry: SectionRegistryStart;
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
}
@ -59,8 +63,7 @@ const SPACE_SETTINGS_ID = 'space-settings';
const GLOBAL_SETTINGS_ID = 'global-settings';
export const Settings = (props: Props) => {
const { componentRegistry, history, settingsService, enableSaving, enableShowing, ...rest } =
props;
const { sectionRegistry, history, settingsService, enableSaving, enableShowing, ...rest } = props;
const uiSettings = settingsService.client;
const globalUiSettings = settingsService.globalClient;
@ -89,7 +92,10 @@ export const Settings = (props: Props) => {
global: {},
namespace: {},
},
footerQueryMatched: false,
filteredSections: {
global: sectionRegistry.getGlobalSections(),
space: sectionRegistry.getSpacesSections(),
},
query: Query.parse(''),
});
@ -206,7 +212,9 @@ export const Settings = (props: Props) => {
categories={categories[scope]}
visibleSettings={queryState.filteredSettings[scope]}
clearQuery={() => setUrlQuery('')}
noResults={!queryState.footerQueryMatched}
noResults={
queryState.filteredSections.global.length + queryState.filteredSections.space.length === 0
}
queryText={queryState.query.text}
callOutTitle={callOutTitle(scope)}
callOutSubtitle={callOutSubtitle(scope)}
@ -225,7 +233,8 @@ export const Settings = (props: Props) => {
append:
queryState.query.text !== '' ? (
<EuiNotificationBadge className="eui-alignCenter" size="m" key="spaceSettings-badge">
{Object.keys(queryState.filteredSettings.namespace).length}
{Object.keys(queryState.filteredSettings.namespace).length +
queryState.filteredSections.space.length}
</EuiNotificationBadge>
) : null,
content: renderAdvancedSettings('namespace'),
@ -239,7 +248,7 @@ export const Settings = (props: Props) => {
queryState.query.text !== '' ? (
<EuiNotificationBadge className="eui-alignCenter" size="m" key="spaceSettings-badge">
{Object.keys(queryState.filteredSettings.global).length +
Number(queryState.footerQueryMatched)}
queryState.filteredSections.global.length}
</EuiNotificationBadge>
) : null,
content: renderAdvancedSettings('global'),
@ -297,7 +306,14 @@ export const Settings = (props: Props) => {
return {
query,
filteredSettings,
footerQueryMatched: initialQuery ? false : queryState.footerQueryMatched,
filteredSections: {
global: sectionRegistry
.getGlobalSections()
.filter(({ queryMatch }) => queryMatch(query.text)),
space: sectionRegistry
.getSpacesSections()
.filter(({ queryMatch }) => queryMatch(query.text)),
},
};
};
@ -308,19 +324,25 @@ export const Settings = (props: Props) => {
[setUrlQuery]
);
const onFooterQueryMatchChange = useCallback(
(matched: boolean) => {
setQueryState({ ...queryState, footerQueryMatched: matched });
},
[queryState]
);
const PageTitle = (
<EuiText>
<h1 data-test-subj="managementSettingsTitle">{i18nTexts.advancedSettingsTitle}</h1>
</EuiText>
);
const PageFooter = componentRegistry.get(componentRegistry.componentType.PAGE_FOOTER_COMPONENT);
const mapSections = (entries: RegistryEntry[]) =>
entries.map(({ Component, queryMatch }, index) => {
if (queryMatch(queryState.query.text)) {
return (
<Component
key={`component-${index}`}
toasts={props.toasts}
enableSaving={props.enableSaving}
/>
);
}
return null;
});
return (
<div>
@ -337,14 +359,11 @@ export const Settings = (props: Props) => {
<EuiSpacer size="m" />
<EuiTabs>{renderTabs()}</EuiTabs>
{selectedTabContent}
{selectedTabId === GLOBAL_SETTINGS_ID ? (
<PageFooter
toasts={props.toasts}
query={queryState.query}
onQueryMatchChange={onFooterQueryMatchChange}
enableSaving={props.enableSaving}
/>
) : null}
{selectedTabId === SPACE_SETTINGS_ID ? (
<>{mapSections(queryState.filteredSections.space)}</>
) : (
<>{mapSections(queryState.filteredSections.global)}</>
)}
</div>
);
};

View file

@ -6,17 +6,21 @@
* Side Public License, v 1.
*/
import { ComponentRegistry } from './component_registry';
import type {
SectionRegistrySetup,
SectionRegistryStart,
} from '@kbn/management-settings-section-registry';
const register = jest.fn();
const get = jest.fn();
const componentType = ComponentRegistry.componentType;
const addGlobalSection = jest.fn();
const addSpaceSection = jest.fn();
const getGlobalSections = jest.fn();
const getSpacesSections = jest.fn();
export const advancedSettingsMock = {
createSetupContract() {
return { component: { register, componentType } };
createSetupContract(): SectionRegistrySetup {
return { addGlobalSection, addSpaceSection };
},
createStartContract() {
return { component: { get, componentType } };
createStartContract(): SectionRegistryStart {
return { getGlobalSections, getSpacesSections };
},
};

View file

@ -8,10 +8,10 @@
import { i18n } from '@kbn/i18n';
import { CoreSetup, Plugin } from '@kbn/core/public';
import { ComponentRegistry } from './component_registry';
import { SectionRegistry } from '@kbn/management-settings-section-registry';
import { AdvancedSettingsSetup, AdvancedSettingsStart, AdvancedSettingsPluginSetup } from './types';
const component = new ComponentRegistry();
const { setup: sectionRegistrySetup, start: sectionRegistryStart } = new SectionRegistry();
const title = i18n.translate('advancedSettings.advancedSettingsLabel', {
defaultMessage: 'Advanced Settings',
@ -37,7 +37,7 @@ export class AdvancedSettingsPlugin
return mountManagementSection(
core.getStartServices,
params,
component.start,
sectionRegistryStart,
usageCollection
);
},
@ -59,13 +59,13 @@ export class AdvancedSettingsPlugin
}
return {
component: component.setup,
...sectionRegistrySetup,
};
}
public start() {
return {
component: component.start,
...sectionRegistryStart,
};
}
}

View file

@ -10,19 +10,16 @@ import { HomePublicPluginSetup } from '@kbn/home-plugin/public';
import { ManagementSetup } from '@kbn/management-plugin/public';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import { ComponentRegistry } from './component_registry';
import type {
SectionRegistrySetup,
SectionRegistryStart,
} from '@kbn/management-settings-section-registry';
export interface AdvancedSettingsSetup {
component: ComponentRegistry['setup'];
}
export interface AdvancedSettingsStart {
component: ComponentRegistry['start'];
}
export type AdvancedSettingsSetup = SectionRegistrySetup;
export type AdvancedSettingsStart = SectionRegistryStart;
export interface AdvancedSettingsPluginSetup {
management: ManagementSetup;
home?: HomePublicPluginSetup;
usageCollection?: UsageCollectionSetup;
}
export { ComponentRegistry };

View file

@ -30,6 +30,8 @@
"@kbn/core-ui-settings-common",
"@kbn/config-schema",
"@kbn/core-plugins-server",
"@kbn/management-settings-section-registry",
"@kbn/react-kibana-context-render",
],
"exclude": [
"target/**/*",

View file

@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
/**
* These are the terms provided to Advanced Settings that map to this section. When searching,
* Advanced Settings will match against these terms to show or hide the section.
*/
export const SEARCH_TERMS: string[] = [
'telemetry',
'usage data', // Keeping this term for BWC
'usage collection',
i18n.translate('telemetry.telemetryConstant', {
defaultMessage: 'telemetry',
}),
i18n.translate('telemetry.usageCollectionConstant', {
defaultMessage: 'usage collection',
}),
].flatMap((term) => {
// Automatically lower-case and split by space the terms from above
const lowerCased = term.toLowerCase();
return [lowerCased, ...lowerCased.split(' ')];
});

View file

@ -6,4 +6,4 @@
* Side Public License, v 1.
*/
export { PageFooter } from './page_footer';
export { SEARCH_TERMS } from './constants';

View file

@ -7,8 +7,8 @@
"server": false,
"browser": true,
"requiredPlugins": [
"advancedSettings",
"telemetry"
"telemetry",
"advancedSettings"
],
"optionalPlugins": [
"usageCollection"
@ -17,4 +17,4 @@
"usageCollection"
]
}
}
}

View file

@ -253,7 +253,6 @@ exports[`TelemetryManagementSectionComponent renders null because allowChangingO
"timeZone": null,
}
}
onQueryMatchChange={[MockFunction]}
showAppliesSettingMessage={true}
telemetryService={
TelemetryService {

View file

@ -22,7 +22,6 @@ describe('TelemetryManagementSectionComponent', () => {
const coreSetup = coreMock.createSetup();
it('renders as expected', () => {
const onQueryMatchChange = jest.fn();
const telemetryService = new TelemetryService({
config: {
appendServerlessChannelsSuffix: false,
@ -44,7 +43,6 @@ describe('TelemetryManagementSectionComponent', () => {
shallowWithIntl(
<TelemetryManagementSection
telemetryService={telemetryService}
onQueryMatchChange={onQueryMatchChange}
showAppliesSettingMessage={true}
enableSaving={true}
toasts={coreStart.notifications.toasts}
@ -55,7 +53,6 @@ describe('TelemetryManagementSectionComponent', () => {
});
it('renders null because query does not match the SEARCH_TERMS', () => {
const onQueryMatchChange = jest.fn();
const telemetryService = new TelemetryService({
config: {
appendServerlessChannelsSuffix: false,
@ -77,7 +74,6 @@ describe('TelemetryManagementSectionComponent', () => {
<React.Suspense fallback={<span>Fallback</span>}>
<TelemetryManagementSection
telemetryService={telemetryService}
onQueryMatchChange={onQueryMatchChange}
showAppliesSettingMessage={false}
enableSaving={true}
toasts={coreStart.notifications.toasts}
@ -90,9 +86,7 @@ describe('TelemetryManagementSectionComponent', () => {
component.rerender(
<React.Suspense fallback={<span>Fallback</span>}>
<TelemetryManagementSection
query={{ text: 'asdasdasd' }}
telemetryService={telemetryService}
onQueryMatchChange={onQueryMatchChange}
showAppliesSettingMessage={false}
enableSaving={true}
toasts={coreStart.notifications.toasts}
@ -100,15 +94,12 @@ describe('TelemetryManagementSectionComponent', () => {
/>
</React.Suspense>
);
expect(onQueryMatchChange).toHaveBeenCalledWith(false);
expect(onQueryMatchChange).toHaveBeenCalledTimes(1);
} finally {
component.unmount();
}
});
it('renders because query matches the SEARCH_TERMS', () => {
const onQueryMatchChange = jest.fn();
const telemetryService = new TelemetryService({
config: {
appendServerlessChannelsSuffix: false,
@ -129,7 +120,6 @@ describe('TelemetryManagementSectionComponent', () => {
const component = mountWithIntl(
<TelemetryManagementSection
telemetryService={telemetryService}
onQueryMatchChange={onQueryMatchChange}
showAppliesSettingMessage={false}
enableSaving={true}
toasts={coreStart.notifications.toasts}
@ -145,17 +135,12 @@ describe('TelemetryManagementSectionComponent', () => {
// It should also render if there is no query at all.
expect(component.setProps({ ...component.props(), query: {} }).html()).not.toBe('');
expect(onQueryMatchChange).toHaveBeenCalledWith(true);
// Should only be called once because the second time does not change the result
expect(onQueryMatchChange).toHaveBeenCalledTimes(1);
} finally {
component.unmount();
}
});
it('renders null because allowChangingOptInStatus is false', () => {
const onQueryMatchChange = jest.fn();
const telemetryService = new TelemetryService({
config: {
appendServerlessChannelsSuffix: false,
@ -176,7 +161,6 @@ describe('TelemetryManagementSectionComponent', () => {
const component = mountWithIntl(
<TelemetryManagementSection
telemetryService={telemetryService}
onQueryMatchChange={onQueryMatchChange}
showAppliesSettingMessage={true}
enableSaving={true}
toasts={coreStart.notifications.toasts}
@ -186,14 +170,12 @@ describe('TelemetryManagementSectionComponent', () => {
try {
expect(component).toMatchSnapshot();
component.setProps({ ...component.props(), query: { text: 'TeLEMetry' } });
expect(onQueryMatchChange).toHaveBeenCalledWith(false);
} finally {
component.unmount();
}
});
it('shows the OptInExampleFlyout', () => {
const onQueryMatchChange = jest.fn();
const telemetryService = new TelemetryService({
config: {
appendServerlessChannelsSuffix: false,
@ -214,7 +196,6 @@ describe('TelemetryManagementSectionComponent', () => {
const component = mountWithIntl(
<TelemetryManagementSection
telemetryService={telemetryService}
onQueryMatchChange={onQueryMatchChange}
showAppliesSettingMessage={false}
enableSaving={true}
toasts={coreStart.notifications.toasts}
@ -235,7 +216,6 @@ describe('TelemetryManagementSectionComponent', () => {
});
it('toggles the OptIn button', async () => {
const onQueryMatchChange = jest.fn();
const telemetryService = new TelemetryService({
config: {
appendServerlessChannelsSuffix: false,
@ -256,7 +236,6 @@ describe('TelemetryManagementSectionComponent', () => {
const component = mountWithIntl(
<TelemetryManagementSection
telemetryService={telemetryService}
onQueryMatchChange={onQueryMatchChange}
showAppliesSettingMessage={false}
enableSaving={true}
toasts={coreStart.notifications.toasts}
@ -284,7 +263,6 @@ describe('TelemetryManagementSectionComponent', () => {
});
it('test the wrapper (for coverage purposes)', () => {
const onQueryMatchChange = jest.fn();
const telemetryService = new TelemetryService({
config: {
appendServerlessChannelsSuffix: false,
@ -307,7 +285,6 @@ describe('TelemetryManagementSectionComponent', () => {
<TelemetryManagementSection
showAppliesSettingMessage={true}
telemetryService={telemetryService}
onQueryMatchChange={onQueryMatchChange}
enableSaving={true}
toasts={coreStart.notifications.toasts}
docLinks={docLinks}

View file

@ -27,28 +27,10 @@ import { OptInExampleFlyout } from './opt_in_example_flyout';
type TelemetryService = TelemetryPluginSetup['telemetryService'];
const SEARCH_TERMS: string[] = [
'telemetry',
'usage data', // Keeping this term for BWC
'usage collection',
i18n.translate('telemetry.telemetryConstant', {
defaultMessage: 'telemetry',
}),
i18n.translate('telemetry.usageCollectionConstant', {
defaultMessage: 'usage collection',
}),
].flatMap((term) => {
// Automatically lower-case and split by space the terms from above
const lowerCased = term.toLowerCase();
return [lowerCased, ...lowerCased.split(' ')];
});
interface Props {
telemetryService: TelemetryService;
onQueryMatchChange: (searchTermMatches: boolean) => void;
showAppliesSettingMessage: boolean;
enableSaving: boolean;
query?: { text: string };
toasts: ToastsStart;
docLinks: DocLinksStart['links'];
}
@ -57,7 +39,6 @@ interface State {
processing: boolean;
showExample: boolean;
showSecurityExample: boolean;
queryMatches: boolean | null;
enabled: boolean;
}
@ -69,47 +50,18 @@ export class TelemetryManagementSection extends Component<Props, State> {
processing: false,
showExample: false,
showSecurityExample: false,
queryMatches: props.query ? this.checkQueryMatch(props.query) : null,
enabled: this.props.telemetryService.getIsOptedIn() || false,
};
}
UNSAFE_componentWillReceiveProps(nextProps: Props) {
const { query } = nextProps;
const queryMatches = this.checkQueryMatch(query);
if (queryMatches !== this.state.queryMatches) {
this.setState(
{
queryMatches,
},
() => {
this.props.onQueryMatchChange(queryMatches);
}
);
}
}
checkQueryMatch(query?: { text: string }): boolean {
const searchTerm = (query?.text ?? '').toLowerCase();
return (
this.props.telemetryService.getCanChangeOptInStatus() &&
SEARCH_TERMS.some((term) => term.indexOf(searchTerm) >= 0)
);
}
render() {
const { telemetryService } = this.props;
const { showExample, queryMatches, enabled, processing } = this.state;
const { showExample, enabled, processing } = this.state;
if (!telemetryService.getCanChangeOptInStatus()) {
return null;
}
if (queryMatches !== null && !queryMatches) {
return null;
}
return (
<Fragment>
{showExample && (

View file

@ -10,6 +10,7 @@ import React, { lazy, Suspense } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import type { TelemetryPluginSetup } from '@kbn/telemetry-plugin/public';
import { DocLinksStart } from '@kbn/core/public';
import { RegistryComponentProps } from '@kbn/management-settings-section-registry';
import type TelemetryManagementSection from './telemetry_management_section';
export type TelemetryManagementSectionWrapperProps = Omit<
@ -23,12 +24,16 @@ export function telemetryManagementSectionWrapper(
telemetryService: TelemetryPluginSetup['telemetryService'],
docLinks: DocLinksStart['links']
) {
const TelemetryManagementSectionWrapper = (props: TelemetryManagementSectionWrapperProps) => (
const TelemetryManagementSectionWrapper = ({
enableSaving,
...props
}: RegistryComponentProps) => (
<Suspense fallback={<EuiLoadingSpinner />}>
<TelemetryManagementSectionComponent
showAppliesSettingMessage={true}
telemetryService={telemetryService}
docLinks={docLinks}
enableSaving={enableSaving.global === true}
{...props}
/>
</Suspense>

View file

@ -12,10 +12,8 @@ import type { TelemetryPluginSetup } from '@kbn/telemetry-plugin/public';
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import type { CoreStart, CoreSetup, DocLinksStart } from '@kbn/core/public';
import {
telemetryManagementSectionWrapper,
TelemetryManagementSectionWrapperProps,
} from './components/telemetry_management_section_wrapper';
import { telemetryManagementSectionWrapper } from './components/telemetry_management_section_wrapper';
import { SEARCH_TERMS } from '../common';
export interface TelemetryManagementSectionPluginDepsSetup {
telemetry: TelemetryPluginSetup;
@ -40,20 +38,22 @@ export class TelemetryManagementSectionPlugin {
const ApplicationUsageTrackingProvider =
usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment;
advancedSettings.component.register(
advancedSettings.component.componentType.PAGE_FOOTER_COMPONENT,
(props) => {
return (
<ApplicationUsageTrackingProvider>
{telemetryManagementSectionWrapper(
telemetryService,
docLinksLinks
)(props as TelemetryManagementSectionWrapperProps)}
</ApplicationUsageTrackingProvider>
);
},
true
);
const queryMatch = (query: string) => {
const searchTerm = query.toLowerCase();
return (
telemetryService.getCanChangeOptInStatus() &&
SEARCH_TERMS.some((term) => term.indexOf(searchTerm) >= 0)
);
};
advancedSettings.addGlobalSection((props) => {
return (
<ApplicationUsageTrackingProvider>
{telemetryManagementSectionWrapper(telemetryService, docLinksLinks)(props)}
</ApplicationUsageTrackingProvider>
);
}, queryMatch);
return {};
}

View file

@ -5,6 +5,7 @@
"isolatedModules": true
},
"include": [
"common/**/*",
"public/**/*",
"../../../typings/**/*"
],
@ -16,6 +17,7 @@
"@kbn/test-jest-helpers",
"@kbn/i18n-react",
"@kbn/i18n",
"@kbn/management-settings-section-registry",
],
"exclude": [
"target/**/*",

View file

@ -950,6 +950,8 @@
"@kbn/management-cards-navigation/*": ["packages/kbn-management/cards_navigation/*"],
"@kbn/management-plugin": ["src/plugins/management"],
"@kbn/management-plugin/*": ["src/plugins/management/*"],
"@kbn/management-settings-section-registry": ["packages/kbn-management/settings/section_registry"],
"@kbn/management-settings-section-registry/*": ["packages/kbn-management/settings/section_registry/*"],
"@kbn/management-storybook-config": ["packages/kbn-management/storybook/config"],
"@kbn/management-storybook-config/*": ["packages/kbn-management/storybook/config/*"],
"@kbn/management-test-plugin": ["test/plugin_functional/plugins/management_test_plugin"],
@ -1562,7 +1564,9 @@
"@kbn/yarn-lock-validator/*": ["packages/kbn-yarn-lock-validator/*"],
// END AUTOMATED PACKAGE LISTING
// Allows for importing from `kibana` package for the exported types.
"@emotion/core": ["typings/@emotion"],
"@emotion/core": [
"typings/@emotion"
],
},
// Support .tsx files and transform JSX into calls to React.createElement
"jsx": "react",
@ -1635,4 +1639,4 @@
"@kbn/ambient-storybook-types"
]
}
}
}

View file

@ -16,7 +16,6 @@
"licensing"
],
"optionalPlugins": [
"advancedSettings",
"home",
"management",
"usageCollection"
@ -29,4 +28,4 @@
"common"
]
}
}
}

View file

@ -1,39 +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 { advancedSettingsMock } from '@kbn/advanced-settings-plugin/public/mocks';
import { AdvancedSettingsService } from './advanced_settings_service';
const componentRegistryMock = advancedSettingsMock.createSetupContract();
describe('Advanced Settings Service', () => {
describe('#setup', () => {
it('registers space-aware components to augment the advanced settings screen', () => {
const deps = {
getActiveSpace: jest.fn().mockResolvedValue({ id: 'foo', name: 'foo-space' }),
componentRegistry: componentRegistryMock.component,
};
const advancedSettingsService = new AdvancedSettingsService();
advancedSettingsService.setup(deps);
expect(deps.componentRegistry.register).toHaveBeenCalledTimes(2);
expect(deps.componentRegistry.register).toHaveBeenCalledWith(
componentRegistryMock.component.componentType.PAGE_TITLE_COMPONENT,
expect.any(Function),
true
);
expect(deps.componentRegistry.register).toHaveBeenCalledWith(
componentRegistryMock.component.componentType.PAGE_SUBTITLE_COMPONENT,
expect.any(Function),
true
);
});
});
});

View file

@ -1,36 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import type { AdvancedSettingsSetup } from '@kbn/advanced-settings-plugin/public';
import { AdvancedSettingsSubtitle, AdvancedSettingsTitle } from './components';
import type { Space } from '../../common';
interface SetupDeps {
getActiveSpace: () => Promise<Space>;
componentRegistry: AdvancedSettingsSetup['component'];
}
export class AdvancedSettingsService {
public setup({ getActiveSpace, componentRegistry }: SetupDeps) {
const PageTitle = () => <AdvancedSettingsTitle getActiveSpace={getActiveSpace} />;
const SubTitle = () => <AdvancedSettingsSubtitle getActiveSpace={getActiveSpace} />;
componentRegistry.register(
componentRegistry.componentType.PAGE_TITLE_COMPONENT,
PageTitle,
true
);
componentRegistry.register(
componentRegistry.componentType.PAGE_SUBTITLE_COMPONENT,
SubTitle,
true
);
}
}

View file

@ -1,36 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiCallOut } from '@elastic/eui';
import { act } from '@testing-library/react';
import React from 'react';
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
import { AdvancedSettingsSubtitle } from './advanced_settings_subtitle';
describe('AdvancedSettingsSubtitle', () => {
it('renders as expected', async () => {
const space = {
id: 'my-space',
name: 'My Space',
disabledFeatures: [],
};
const wrapper = mountWithIntl(
<AdvancedSettingsSubtitle getActiveSpace={() => Promise.resolve(space)} />
);
// Wait for active space to resolve before requesting the component to update
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find(EuiCallOut)).toHaveLength(1);
});
});

View file

@ -1,46 +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 { EuiCallOut, EuiSpacer } from '@elastic/eui';
import React, { Fragment, useEffect, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import type { Space } from '../../../../common';
interface Props {
getActiveSpace: () => Promise<Space>;
}
export const AdvancedSettingsSubtitle = (props: Props) => {
const [activeSpace, setActiveSpace] = useState<Space | null>(null);
useEffect(() => {
props.getActiveSpace().then((space) => setActiveSpace(space));
}, [props]);
if (!activeSpace) return null;
return (
<Fragment>
<EuiSpacer size={'m'} />
<EuiCallOut
color="primary"
iconType="spacesApp"
title={
<FormattedMessage
id="xpack.spaces.management.advancedSettingsSubtitle.applyingSettingsOnPageToSpaceDescription"
defaultMessage="The settings on this page apply to the {spaceName} space, unless otherwise specified."
values={{
spaceName: <strong>{activeSpace.name}</strong>,
}}
/>
}
/>
</Fragment>
);
};

View file

@ -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 { AdvancedSettingsSubtitle } from './advanced_settings_subtitle';

View file

@ -1,36 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { act } from '@testing-library/react';
import React from 'react';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { AdvancedSettingsTitle } from './advanced_settings_title';
import { SpaceAvatarInternal } from '../../../space_avatar/space_avatar_internal';
describe('AdvancedSettingsTitle', () => {
it('renders without crashing', async () => {
const space = {
id: 'my-space',
name: 'My Space',
disabledFeatures: [],
};
const wrapper = mountWithIntl(
<AdvancedSettingsTitle getActiveSpace={() => Promise.resolve(space)} />
);
await act(async () => {});
// wait for SpaceAvatar to lazy-load
await act(async () => {});
wrapper.update();
expect(wrapper.find(SpaceAvatarInternal)).toHaveLength(1);
});
});

View file

@ -1,53 +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 { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiTitle } from '@elastic/eui';
import React, { lazy, Suspense, useEffect, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import type { Space } from '../../../../common';
import { getSpaceAvatarComponent } from '../../../space_avatar';
// No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana.
const LazySpaceAvatar = lazy(() =>
getSpaceAvatarComponent().then((component) => ({ default: component }))
);
interface Props {
getActiveSpace: () => Promise<Space>;
}
export const AdvancedSettingsTitle = (props: Props) => {
const [activeSpace, setActiveSpace] = useState<Space | null>(null);
useEffect(() => {
props.getActiveSpace().then((space) => setActiveSpace(space));
}, [props]);
if (!activeSpace) return null;
return (
<EuiFlexGroup gutterSize="s" responsive={false} alignItems={'center'}>
<EuiFlexItem grow={false}>
<Suspense fallback={<EuiLoadingSpinner />}>
<LazySpaceAvatar space={activeSpace} />
</Suspense>
</EuiFlexItem>
<EuiFlexItem style={{ marginLeft: '10px' }}>
<EuiTitle size="m">
<h1 data-test-subj="managementSettingsTitle">
<FormattedMessage
id="xpack.spaces.management.advancedSettingsTitle.settingsTitle"
defaultMessage="Settings"
/>
</h1>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -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 { AdvancedSettingsTitle } from './advanced_settings_title';

View file

@ -1,9 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { AdvancedSettingsSubtitle } from './advanced_settings_subtitle';
export { AdvancedSettingsTitle } from './advanced_settings_title';

View file

@ -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 { AdvancedSettingsService } from './advanced_settings_service';

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { advancedSettingsMock } from '@kbn/advanced-settings-plugin/public/mocks';
import { coreMock } from '@kbn/core/public/mocks';
import { homePluginMock } from '@kbn/home-plugin/public/mocks';
import {
@ -63,29 +62,6 @@ describe('Spaces plugin', () => {
})
);
});
it('should register the advanced settings components if the advanced_settings plugin is available', () => {
const coreSetup = coreMock.createSetup();
const advancedSettings = advancedSettingsMock.createSetupContract();
const plugin = new SpacesPlugin(coreMock.createPluginInitializerContext());
plugin.setup(coreSetup, { advancedSettings });
expect(advancedSettings.component.register.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"advanced_settings_page_title",
[Function],
true,
],
Array [
"advanced_settings_page_subtitle",
[Function],
true,
],
]
`);
});
});
describe('#start', () => {

View file

@ -5,13 +5,11 @@
* 2.0.
*/
import type { AdvancedSettingsSetup } from '@kbn/advanced-settings-plugin/public';
import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public';
import type { FeaturesPluginStart } from '@kbn/features-plugin/public';
import type { HomePublicPluginSetup } from '@kbn/home-plugin/public';
import type { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public';
import { AdvancedSettingsService } from './advanced_settings';
import type { ConfigType } from './config';
import { createSpacesFeatureCatalogueEntry } from './create_feature_catalogue_entry';
import { ManagementService } from './management';
@ -22,7 +20,6 @@ import type { SpacesApi } from './types';
import { getUiApi } from './ui_api';
export interface PluginsSetup {
advancedSettings?: AdvancedSettingsSetup;
home?: HomePublicPluginSetup;
management?: ManagementSetup;
}
@ -78,14 +75,6 @@ export class SpacesPlugin implements Plugin<SpacesPluginSetup, SpacesPluginStart
});
}
if (plugins.advancedSettings) {
const advancedSettingsService = new AdvancedSettingsService();
advancedSettingsService.setup({
getActiveSpace: () => this.spacesManager.getActiveSpace(),
componentRegistry: plugins.advancedSettings.component,
});
}
spaceSelectorApp.create({
getStartServices: core.getStartServices,
application: core.application,

View file

@ -8,7 +8,6 @@
"@kbn/features-plugin",
"@kbn/licensing-plugin",
"@kbn/es-ui-shared-plugin",
"@kbn/advanced-settings-plugin",
"@kbn/home-plugin",
"@kbn/kibana-react-plugin",
"@kbn/management-plugin",

View file

@ -130,7 +130,6 @@
"advancedSettings.globalCalloutSubtitle": "Les modifications seront appliquées à tous les utilisateurs dans l'ensemble des espaces. Cela inclut les utilisateurs Kibana natifs et les utilisateurs qui se connectent via l'authentification unique.",
"advancedSettings.globalCalloutTitle": "Les modifications auront une incidence sur tous les paramètres utilisateur dans l'ensemble des espaces",
"advancedSettings.globalSettingsTabTitle": "Paramètres généraux",
"advancedSettings.pageTitle": "Paramètres",
"advancedSettings.searchBar.unableToParseQueryErrorMessage": "Impossible d'analyser la requête",
"advancedSettings.searchBarAriaLabel": "Rechercher dans les paramètres avancés",
"advancedSettings.spaceSettingsTabTitle": "Paramètres de l'espace",
@ -35641,7 +35640,6 @@
"xpack.spaces.legacyUrlConflict.calloutBodyText": "Assurez-vous qu'il s'agit du {objectNoun} que vous recherchez. Sinon, consultez l'autre. {documentationLink}",
"xpack.spaces.legacyUrlConflict.linkButton": "Accéder à un autre {objectNoun}",
"xpack.spaces.legacyURLConflict.toolTipText": "Ce {objectNoun} possède l'ID [id={currentObjectId}]. L'autre {objectNoun} possède l'ID [id={otherObjectId}].",
"xpack.spaces.management.advancedSettingsSubtitle.applyingSettingsOnPageToSpaceDescription": "Les paramètres de cette page s'appliquent à l'espace {spaceName}, sauf indication contraire.",
"xpack.spaces.management.confirmDeleteModal.confirmButton": "{isLoading, select, true {Suppression de l'espace et de tous les contenus…} other {Supprimer l'espace et tous les contenus}}",
"xpack.spaces.management.confirmDeleteModal.description": "Cet espace et {allContents} seront définitivement supprimés.",
"xpack.spaces.management.copyToSpace.copyStatusSummary.conflictsMessage": "Conflits détectés dans l'espace {space}. Développez cette section pour les résoudre.",
@ -35685,7 +35683,6 @@
"xpack.spaces.legacyUrlConflict.calloutTitle": "2 objets enregistrés utilisent cette URL",
"xpack.spaces.legacyUrlConflict.dismissButton": "Rejeter",
"xpack.spaces.legacyUrlConflict.documentationLinkText": "En savoir plus",
"xpack.spaces.management.advancedSettingsTitle.settingsTitle": "Paramètres",
"xpack.spaces.management.confirmAlterActiveSpaceModal.cancelButton": "Annuler",
"xpack.spaces.management.confirmAlterActiveSpaceModal.reloadWarningMessage": "Vous avez mis à jour les fonctionnalités visibles dans cet espace. Votre page sera rechargée après l'enregistrement.",
"xpack.spaces.management.confirmAlterActiveSpaceModal.title": "Confirmer la mise à jour de l'espace",

View file

@ -130,7 +130,6 @@
"advancedSettings.globalCalloutSubtitle": "変更はすべてのスペースのすべてのユーザーに適用されます。これにはネイティブKibanaユーザーとシングルサインオンユーザーの両方が含まれます。",
"advancedSettings.globalCalloutTitle": "変更はすべてのスペースのすべてのユーザー設定に影響します",
"advancedSettings.globalSettingsTabTitle": "グローバル設定",
"advancedSettings.pageTitle": "設定",
"advancedSettings.searchBar.unableToParseQueryErrorMessage": "クエリをパースできません",
"advancedSettings.searchBarAriaLabel": "高度な設定を検索",
"advancedSettings.spaceSettingsTabTitle": "スペース設定",
@ -35640,7 +35639,6 @@
"xpack.spaces.legacyUrlConflict.calloutBodyText": "これが検索している{objectNoun}であることを確認してください。そうでない場合は、他の項目に移動します。{documentationLink}",
"xpack.spaces.legacyUrlConflict.linkButton": "他の{objectNoun}に移動",
"xpack.spaces.legacyURLConflict.toolTipText": "この{objectNoun}は[id={currentObjectId}]があります。他のは{objectNoun}[id={otherObjectId}]があります。",
"xpack.spaces.management.advancedSettingsSubtitle.applyingSettingsOnPageToSpaceDescription": "このページの設定は、別途指定されていない限り{spaceName}スペースに適用されます。",
"xpack.spaces.management.confirmDeleteModal.confirmButton": "{isLoading, select, true {スペースとすべてのコンテンツを削除中...} other {スペースとすべてのコンテンツを削除}}",
"xpack.spaces.management.confirmDeleteModal.description": "このスペースと{allContents}は完全に削除されます。",
"xpack.spaces.management.copyToSpace.copyStatusSummary.conflictsMessage": "{space}スペースで競合が検出されました。解決するにはこのセクションを拡張してください。",
@ -35684,7 +35682,6 @@
"xpack.spaces.legacyUrlConflict.calloutTitle": "2つの保存されたオブジェクトがこのURLを使用します",
"xpack.spaces.legacyUrlConflict.dismissButton": "閉じる",
"xpack.spaces.legacyUrlConflict.documentationLinkText": "詳細",
"xpack.spaces.management.advancedSettingsTitle.settingsTitle": "設定",
"xpack.spaces.management.confirmAlterActiveSpaceModal.cancelButton": "キャンセル",
"xpack.spaces.management.confirmAlterActiveSpaceModal.reloadWarningMessage": "このスペースで表示される機能を更新しました。保存後にページが更新されます。",
"xpack.spaces.management.confirmAlterActiveSpaceModal.title": "スペースの更新の確認",

View file

@ -130,7 +130,6 @@
"advancedSettings.globalCalloutSubtitle": "将对所有工作区的所有用户应用更改。这包括本机 Kibana 用户和单点登录用户。",
"advancedSettings.globalCalloutTitle": "更改将影响所有工作区的所有用户设置",
"advancedSettings.globalSettingsTabTitle": "常规设置",
"advancedSettings.pageTitle": "设置",
"advancedSettings.searchBar.unableToParseQueryErrorMessage": "无法解析查询",
"advancedSettings.searchBarAriaLabel": "搜索高级设置",
"advancedSettings.spaceSettingsTabTitle": "工作区设置",
@ -35634,7 +35633,6 @@
"xpack.spaces.legacyUrlConflict.calloutBodyText": "检查这是否是您正寻找的 {objectNoun}。否则,请前往另一个。{documentationLink}",
"xpack.spaces.legacyUrlConflict.linkButton": "前往其他 {objectNoun}",
"xpack.spaces.legacyURLConflict.toolTipText": "此 {objectNoun} 具有 [id={currentObjectId}]。其他 {objectNoun} 具有 [id={otherObjectId}]。",
"xpack.spaces.management.advancedSettingsSubtitle.applyingSettingsOnPageToSpaceDescription": "除非已指定,否则此页面上的设置适用于 {spaceName} 空间。",
"xpack.spaces.management.confirmDeleteModal.confirmButton": "{isLoading, select, true {正在删除工作区和所有内容……} other {删除工作区和所有内容}}",
"xpack.spaces.management.confirmDeleteModal.description": "此工作区和{allContents}将被永久删除。",
"xpack.spaces.management.copyToSpace.copyStatusSummary.conflictsMessage": "在 {space} 工作区中检测到冲突。展开此部分可进行解决。",
@ -35678,7 +35676,6 @@
"xpack.spaces.legacyUrlConflict.calloutTitle": "2 个已保存对象使用此 URL",
"xpack.spaces.legacyUrlConflict.dismissButton": "关闭",
"xpack.spaces.legacyUrlConflict.documentationLinkText": "了解详情",
"xpack.spaces.management.advancedSettingsTitle.settingsTitle": "设置",
"xpack.spaces.management.confirmAlterActiveSpaceModal.cancelButton": "取消",
"xpack.spaces.management.confirmAlterActiveSpaceModal.reloadWarningMessage": "您已更新此工作区中的可见功能。保存后,您的页面将重新加载。",
"xpack.spaces.management.confirmAlterActiveSpaceModal.title": "确认更新工作区",

View file

@ -4784,6 +4784,10 @@
version "0.0.0"
uid ""
"@kbn/management-settings-section-registry@link:packages/kbn-management/settings/section_registry":
version "0.0.0"
uid ""
"@kbn/management-storybook-config@link:packages/kbn-management/storybook/config":
version "0.0.0"
uid ""