Introduce start lifecycle event to client (#35269)

This commit is contained in:
Josh Dover 2019-04-23 11:50:08 -05:00 committed by GitHub
parent 9947045f69
commit c2f4123b8c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
104 changed files with 1267 additions and 651 deletions

View file

@ -16,30 +16,30 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Capabilities, CapabilitiesService, CapabilitiesSetup } from './capabilities_service';
import { Capabilities, CapabilitiesService, CapabilitiesStart } from './capabilities_service';
const createSetupContractMock = () => {
const setupContract: jest.Mocked<CapabilitiesSetup> = {
const createStartContractMock = () => {
const startContract: jest.Mocked<CapabilitiesStart> = {
getCapabilities: jest.fn(),
};
setupContract.getCapabilities.mockReturnValue({
startContract.getCapabilities.mockReturnValue({
catalogue: {},
management: {},
navLinks: {},
} as Capabilities);
return setupContract;
return startContract;
};
type CapabilitiesServiceContract = PublicMethodsOf<CapabilitiesService>;
const createMock = () => {
const mocked: jest.Mocked<CapabilitiesServiceContract> = {
setup: jest.fn(),
start: jest.fn(),
};
mocked.setup.mockReturnValue(createSetupContractMock());
mocked.start.mockReturnValue(createStartContractMock());
return mocked;
};
export const capabilitiesServiceMock = {
create: createMock,
createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
};

View file

@ -32,7 +32,7 @@ describe('#start', () => {
} as any,
});
const service = new CapabilitiesService();
const startContract = service.setup({ injectedMetadata: injectedMetadata.setup() });
const startContract = service.start({ injectedMetadata: injectedMetadata.start() });
expect(startContract.getCapabilities()).toEqual({
foo: 'bar',
bar: 'baz',
@ -51,7 +51,7 @@ describe('#start', () => {
} as any,
});
const service = new CapabilitiesService();
const startContract = service.setup({ injectedMetadata: injectedMetadata.setup() });
const startContract = service.start({ injectedMetadata: injectedMetadata.start() });
const capabilities = startContract.getCapabilities();
// @ts-ignore TypeScript knows this shouldn't be possible

View file

@ -16,11 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
import { InjectedMetadataSetup } from '../injected_metadata';
import { InjectedMetadataStart } from '../injected_metadata';
import { deepFreeze } from '../utils/deep_freeze';
interface StartDeps {
injectedMetadata: InjectedMetadataSetup;
injectedMetadata: InjectedMetadataStart;
}
/**
@ -50,7 +50,7 @@ export interface Capabilities {
* Capabilities Setup.
* @public
*/
export interface CapabilitiesSetup {
export interface CapabilitiesStart {
/**
* Gets the read-only capabilities.
*/
@ -63,10 +63,10 @@ export interface CapabilitiesSetup {
* Service that is responsible for UI Capabilities.
*/
export class CapabilitiesService {
public setup({ injectedMetadata }: StartDeps): CapabilitiesSetup {
public start({ injectedMetadata }: StartDeps): CapabilitiesStart {
return {
getCapabilities: () =>
deepFreeze<Capabilities>(injectedMetadata.getInjectedVar('uiCapabilities') as Capabilities),
deepFreeze(injectedMetadata.getInjectedVar('uiCapabilities') as Capabilities),
};
}
}

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { Capabilities, CapabilitiesService, CapabilitiesSetup } from './capabilities_service';
export { Capabilities, CapabilitiesService, CapabilitiesStart } from './capabilities_service';

View file

@ -18,6 +18,7 @@
*/
import { basePathServiceMock } from './base_path/base_path_service.mock';
import { capabilitiesServiceMock } from './capabilities/capabilities_service.mock';
import { chromeServiceMock } from './chrome/chrome_service.mock';
import { fatalErrorsServiceMock } from './fatal_errors/fatal_errors_service.mock';
import { httpServiceMock } from './http/http_service.mock';
@ -104,3 +105,11 @@ export const PluginsServiceConstructor = jest.fn().mockImplementation(() => Mock
jest.doMock('./plugins', () => ({
PluginsService: PluginsServiceConstructor,
}));
export const MockCapabilitiesService = capabilitiesServiceMock.create();
export const CapabilitiesServiceConstructor = jest
.fn()
.mockImplementation(() => MockCapabilitiesService);
jest.doMock('./capabilities', () => ({
CapabilitiesService: CapabilitiesServiceConstructor,
}));

View file

@ -17,9 +17,6 @@
* under the License.
*/
import { Observable } from 'rxjs';
import { take } from 'rxjs/operators';
import {
BasePathServiceConstructor,
ChromeServiceConstructor,
@ -42,6 +39,7 @@ import {
NotificationServiceConstructor,
OverlayServiceConstructor,
UiSettingsServiceConstructor,
MockCapabilitiesService,
} from './core_system.test.mocks';
import { CoreSystem } from './core_system';
@ -110,21 +108,11 @@ describe('constructor', () => {
expect(LegacyPlatformServiceConstructor).toHaveBeenCalledTimes(1);
expect(LegacyPlatformServiceConstructor).toHaveBeenCalledWith({
targetDomElement: expect.any(HTMLElement),
requireLegacyFiles,
useLegacyTestHarness,
});
});
it('passes a dom element to NotificationsService', () => {
createCoreSystem();
expect(NotificationServiceConstructor).toHaveBeenCalledTimes(1);
expect(NotificationServiceConstructor).toHaveBeenCalledWith({
targetDomElement$: expect.any(Observable),
});
});
it('passes browserSupportsCsp to ChromeService', () => {
createCoreSystem();
@ -157,7 +145,121 @@ describe('constructor', () => {
});
});
describe('#stop', () => {
describe('#setup()', () => {
function setupCore(rootDomElement = defaultCoreSystemParams.rootDomElement) {
const core = createCoreSystem({
...defaultCoreSystemParams,
rootDomElement,
});
return core.setup();
}
it('calls injectedMetadata#setup()', async () => {
await setupCore();
expect(MockInjectedMetadataService.setup).toHaveBeenCalledTimes(1);
});
it('calls http#setup()', async () => {
await setupCore();
expect(MockHttpService.setup).toHaveBeenCalledTimes(1);
});
it('calls basePath#setup()', async () => {
await setupCore();
expect(MockBasePathService.setup).toHaveBeenCalledTimes(1);
});
it('calls uiSettings#setup()', async () => {
await setupCore();
expect(MockUiSettingsService.setup).toHaveBeenCalledTimes(1);
});
it('calls i18n#setup()', async () => {
await setupCore();
expect(MockI18nService.setup).toHaveBeenCalledTimes(1);
});
it('calls fatalErrors#setup()', async () => {
await setupCore();
expect(MockFatalErrorsService.setup).toHaveBeenCalledTimes(1);
});
it('calls notifications#setup()', async () => {
await setupCore();
expect(MockNotificationsService.setup).toHaveBeenCalledTimes(1);
});
it('calls chrome#setup()', async () => {
await setupCore();
expect(MockChromeService.setup).toHaveBeenCalledTimes(1);
});
it('calls plugin#setup()', async () => {
await setupCore();
expect(MockPluginsService.setup).toHaveBeenCalledTimes(1);
});
});
describe('#start()', () => {
async function startCore(rootDomElement = defaultCoreSystemParams.rootDomElement) {
const core = createCoreSystem({
...defaultCoreSystemParams,
rootDomElement,
});
await core.setup();
await core.start();
}
it('clears the children of the rootDomElement and appends container for legacyPlatform and notifications', async () => {
const root = document.createElement('div');
root.innerHTML = '<p>foo bar</p>';
await startCore(root);
expect(root.innerHTML).toBe('<div></div><div></div><div></div>');
});
it('calls capabilities#start()', async () => {
await startCore();
expect(MockCapabilitiesService.start).toHaveBeenCalledTimes(1);
});
it('calls i18n#start()', async () => {
await startCore();
expect(MockI18nService.start).toHaveBeenCalledTimes(1);
});
it('calls injectedMetadata#start()', async () => {
await startCore();
expect(MockInjectedMetadataService.start).toHaveBeenCalledTimes(1);
});
it('calls notifications#start() with a dom element', async () => {
await startCore();
expect(MockNotificationsService.start).toHaveBeenCalledTimes(1);
expect(MockNotificationsService.start).toHaveBeenCalledWith({
i18n: expect.any(Object),
targetDomElement: expect.any(HTMLElement),
});
});
it('calls plugins#start()', async () => {
await startCore();
expect(MockPluginsService.start).toHaveBeenCalledTimes(1);
});
it('calls legacyPlatform#start()', async () => {
await startCore();
expect(MockLegacyPlatformService.start).toHaveBeenCalledTimes(1);
});
it('calls overlays#start()', async () => {
await startCore();
expect(MockOverlayService.start).toHaveBeenCalledTimes(1);
});
});
describe('#stop()', () => {
it('calls legacyPlatform.stop()', () => {
const coreSystem = createCoreSystem();
@ -212,126 +314,50 @@ describe('#stop', () => {
});
await coreSystem.setup();
await coreSystem.start();
expect(rootDomElement.innerHTML).not.toBe('');
await coreSystem.stop();
expect(rootDomElement.innerHTML).toBe('');
});
});
describe('#setup()', () => {
function setupCore(rootDomElement = defaultCoreSystemParams.rootDomElement) {
const core = createCoreSystem({
...defaultCoreSystemParams,
rootDomElement,
});
return core.setup();
}
it('clears the children of the rootDomElement and appends container for legacyPlatform and notifications', async () => {
const root = document.createElement('div');
root.innerHTML = '<p>foo bar</p>';
await setupCore(root);
expect(root.innerHTML).toBe('<div></div><div></div><div></div>');
});
it('calls injectedMetadata#setup()', async () => {
await setupCore();
expect(MockInjectedMetadataService.setup).toHaveBeenCalledTimes(1);
});
it('calls http#setup()', async () => {
await setupCore();
expect(MockHttpService.setup).toHaveBeenCalledTimes(1);
});
it('calls basePath#setup()', async () => {
await setupCore();
expect(MockBasePathService.setup).toHaveBeenCalledTimes(1);
});
it('calls uiSettings#setup()', async () => {
await setupCore();
expect(MockUiSettingsService.setup).toHaveBeenCalledTimes(1);
});
it('calls i18n#setup()', async () => {
await setupCore();
expect(MockI18nService.setup).toHaveBeenCalledTimes(1);
});
it('calls fatalErrors#setup()', async () => {
await setupCore();
expect(MockFatalErrorsService.setup).toHaveBeenCalledTimes(1);
});
it('calls notifications#setup()', async () => {
await setupCore();
expect(MockNotificationsService.setup).toHaveBeenCalledTimes(1);
});
it('calls chrome#setup()', async () => {
await setupCore();
expect(MockChromeService.setup).toHaveBeenCalledTimes(1);
});
it('calls overlays#setup()', () => {
setupCore();
expect(MockOverlayService.setup).toHaveBeenCalledTimes(1);
});
it('calls plugin#setup()', async () => {
await setupCore();
expect(MockPluginsService.setup).toHaveBeenCalledTimes(1);
});
});
describe('LegacyPlatform targetDomElement', () => {
it('only mounts the element when set up, before setting up the legacyPlatformService', async () => {
it('only mounts the element when start, after setting up the legacyPlatformService', async () => {
const rootDomElement = document.createElement('div');
const core = createCoreSystem({
rootDomElement,
});
let targetDomElementParentInSetup: HTMLElement;
MockLegacyPlatformService.setup.mockImplementation(() => {
targetDomElementParentInSetup = targetDomElement.parentElement;
let targetDomElementParentInStart: HTMLElement | null;
MockLegacyPlatformService.start.mockImplementation(async ({ targetDomElement }) => {
targetDomElementParentInStart = targetDomElement.parentElement;
});
// targetDomElement should not have a parent element when the LegacyPlatformService is constructed
const [[{ targetDomElement }]] = LegacyPlatformServiceConstructor.mock.calls;
expect(targetDomElement).toHaveProperty('parentElement', null);
// setting up the core system should mount the targetDomElement as a child of the rootDomElement
await core.setup();
expect(targetDomElementParentInSetup!).toBe(rootDomElement);
await core.start();
expect(targetDomElementParentInStart!).toBe(rootDomElement);
});
});
describe('Notifications targetDomElement', () => {
it('only mounts the element when set up, before setting up the notificationsService', async () => {
it('only mounts the element when started, after setting up the notificationsService', async () => {
const rootDomElement = document.createElement('div');
const core = createCoreSystem({
rootDomElement,
});
const [[{ targetDomElement$ }]] = NotificationServiceConstructor.mock.calls;
let targetDomElementParentInSetup: HTMLElement | null;
MockNotificationsService.setup.mockImplementation(
(): any => {
(targetDomElement$ as Observable<HTMLElement>).pipe(take(1)).subscribe({
next: targetDomElement => {
// The targetDomElement should already be a child once it's received by the NotificationsService
expect(targetDomElement.parentElement).not.toBeNull();
targetDomElementParentInSetup = targetDomElement.parentElement;
},
});
let targetDomElementParentInStart: HTMLElement | null;
MockNotificationsService.start.mockImplementation(
({ targetDomElement }): any => {
expect(targetDomElement.parentElement).not.toBeNull();
targetDomElementParentInStart = targetDomElement.parentElement;
}
);
// setting up the core system should mount the targetDomElement as a child of the rootDomElement
// setting up and starting the core system should mount the targetDomElement as a child of the rootDomElement
await core.setup();
expect(targetDomElementParentInSetup!).toBe(rootDomElement);
await core.start();
expect(targetDomElementParentInStart!).toBe(rootDomElement);
});
});

View file

@ -19,9 +19,7 @@
import './core.css';
import { Subject } from 'rxjs';
import { CoreSetup } from '.';
import { CoreSetup, CoreStart } from '.';
import { BasePathService } from './base_path';
import { CapabilitiesService } from './capabilities';
import { ChromeService } from './chrome';
@ -70,8 +68,6 @@ export class CoreSystem {
private readonly plugins: PluginsService;
private readonly rootDomElement: HTMLElement;
private readonly notificationsTargetDomElement$: Subject<HTMLDivElement>;
private readonly legacyPlatformTargetDomElement: HTMLDivElement;
private readonly overlayTargetDomElement: HTMLDivElement;
constructor(params: Params) {
@ -101,10 +97,7 @@ export class CoreSystem {
},
});
this.notificationsTargetDomElement$ = new Subject();
this.notifications = new NotificationsService({
targetDomElement$: this.notificationsTargetDomElement$.asObservable(),
});
this.notifications = new NotificationsService();
this.http = new HttpService();
this.basePath = new BasePathService();
this.uiSettings = new UiSettingsService();
@ -115,9 +108,7 @@ export class CoreSystem {
const core: CoreContext = {};
this.plugins = new PluginsService(core);
this.legacyPlatformTargetDomElement = document.createElement('div');
this.legacyPlatform = new LegacyPlatformService({
targetDomElement: this.legacyPlatformTargetDomElement,
requireLegacyFiles,
useLegacyTestHarness,
});
@ -129,15 +120,13 @@ export class CoreSystem {
const injectedMetadata = this.injectedMetadata.setup();
const fatalErrors = this.fatalErrors.setup({ i18n });
const http = this.http.setup({ fatalErrors });
const overlays = this.overlay.setup({ i18n });
const basePath = this.basePath.setup({ injectedMetadata });
const capabilities = this.capabilities.setup({ injectedMetadata });
const uiSettings = this.uiSettings.setup({
http,
injectedMetadata,
basePath,
});
const notifications = this.notifications.setup({ i18n, uiSettings });
const notifications = this.notifications.setup({ uiSettings });
const chrome = this.chrome.setup({
injectedMetadata,
notifications,
@ -149,31 +138,52 @@ export class CoreSystem {
fatalErrors,
http,
i18n,
capabilities,
injectedMetadata,
notifications,
uiSettings,
overlays,
};
// Services that do not expose contracts at setup
await this.plugins.setup(core);
await this.legacyPlatform.setup({ core });
return { fatalErrors };
} catch (error) {
this.fatalErrors.add(error);
}
}
public async start() {
try {
// ensure the rootDomElement is empty
this.rootDomElement.textContent = '';
this.rootDomElement.classList.add('coreSystemRootDomElement');
const notificationsTargetDomElement = document.createElement('div');
const legacyPlatformTargetDomElement = document.createElement('div');
this.rootDomElement.appendChild(notificationsTargetDomElement);
this.rootDomElement.appendChild(this.legacyPlatformTargetDomElement);
this.rootDomElement.appendChild(legacyPlatformTargetDomElement);
this.rootDomElement.appendChild(this.overlayTargetDomElement);
// Only provide the DOM element to notifications once it's attached to the page.
// This prevents notifications from timing out before being displayed.
this.notificationsTargetDomElement$.next(notificationsTargetDomElement);
const injectedMetadata = this.injectedMetadata.start();
const i18n = this.i18n.start();
const capabilities = this.capabilities.start({ injectedMetadata });
const notifications = this.notifications.start({
i18n,
targetDomElement: notificationsTargetDomElement,
});
const overlays = this.overlay.start({ i18n });
this.legacyPlatform.setup(core);
const core: CoreStart = {
capabilities,
i18n,
injectedMetadata,
notifications,
overlays,
};
return { fatalErrors };
await this.plugins.start(core);
await this.legacyPlatform.start({ core, targetDomElement: legacyPlatformTargetDomElement });
} catch (error) {
this.fatalErrors.add(error);
}

View file

@ -58,3 +58,62 @@ exports[`#setup() returns \`Context\` component 1`] = `
</MockEuiContext>
</MockI18nProvider>
`;
exports[`#start() returns \`Context\` component 1`] = `
<MockI18nProvider>
<MockEuiContext
i18n={
Object {
"mapping": Object {
"euiBasicTable.selectAllRows": "Select all rows",
"euiBasicTable.selectThisRow": "Select this row",
"euiBasicTable.tableDescription": [Function],
"euiBottomBar.screenReaderAnnouncement": "There is a new menu opening with page level controls at the end of the document.",
"euiCodeBlock.copyButton": "Copy",
"euiCodeEditor.startEditing": "Press Enter to start editing.",
"euiCodeEditor.startInteracting": "Press Enter to start interacting with the code.",
"euiCodeEditor.stopEditing": "When you're done, press Escape to stop editing.",
"euiCodeEditor.stopInteracting": "When you're done, press Escape to stop interacting with the code.",
"euiCollapsedItemActions.allActions": "All actions",
"euiColorPicker.colorSelectionLabel": [Function],
"euiColorPicker.transparentColor": "transparent",
"euiComboBoxOptionsList.allOptionsSelected": "You've selected all available options",
"euiComboBoxOptionsList.alreadyAdded": [Function],
"euiComboBoxOptionsList.createCustomOption": [Function],
"euiComboBoxOptionsList.loadingOptions": "Loading options",
"euiComboBoxOptionsList.noAvailableOptions": "There aren't any options available",
"euiComboBoxOptionsList.noMatchingOptions": [Function],
"euiComboBoxPill.removeSelection": [Function],
"euiForm.addressFormErrors": "Please address the errors in your form.",
"euiFormControlLayoutClearButton.label": "Clear input",
"euiHeaderAlert.dismiss": "Dismiss",
"euiHeaderLinks.appNavigation": "App navigation",
"euiHeaderLinks.openNavigationMenu": "Open navigation menu",
"euiModal.closeModal": "Closes this modal window",
"euiPagination.jumpToLastPage": [Function],
"euiPagination.nextPage": "Next page",
"euiPagination.pageOfTotal": [Function],
"euiPagination.previousPage": "Previous page",
"euiPopover.screenReaderAnnouncement": "You are in a popup. To exit this popup, hit Escape.",
"euiStep.completeStep": "Step",
"euiStep.incompleteStep": "Incomplete Step",
"euiStepHorizontal.buttonTitle": [Function],
"euiStepHorizontal.step": "Step",
"euiStepNumber.hasErrors": "has errors",
"euiStepNumber.hasWarnings": "has warnings",
"euiStepNumber.isComplete": "complete",
"euiSuperSelect.screenReaderAnnouncement": [Function],
"euiSuperSelectControl.selectAnOption": [Function],
"euiTablePagination.rowsPerPage": "Rows per page",
"euiTableSortMobile.sorting": "Sorting",
"euiToast.dismissToast": "Dismiss toast",
"euiToast.newNotification": "A new notification appears",
"euiToast.notification": "Notification",
},
}
}
>
content
</MockEuiContext>
</MockI18nProvider>
`;

View file

@ -30,17 +30,23 @@ const createSetupContractMock = () => {
return setupContract;
};
// Start contract is identical to setup
const createStartContractMock = createSetupContractMock;
type I18nServiceContract = PublicMethodsOf<I18nService>;
const createMock = () => {
const mocked: jest.Mocked<I18nServiceContract> = {
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
};
mocked.setup.mockReturnValue(createSetupContractMock());
mocked.start.mockReturnValue(createStartContractMock());
return mocked;
};
export const i18nServiceMock = {
create: createMock,
createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
};

View file

@ -53,3 +53,13 @@ describe('#setup()', () => {
expect(shallow(<i18n.Context>content</i18n.Context>)).toMatchSnapshot();
});
});
describe('#start()', () => {
it('returns `Context` component', () => {
const i18nService = new I18nService();
const i18n = i18nService.start();
expect(shallow(<i18n.Context>content</i18n.Context>)).toMatchSnapshot();
});
});

View file

@ -267,6 +267,10 @@ export class I18nService {
return setup;
}
public start() {
return this.setup();
}
public stop() {
// nothing to do here currently
}
@ -285,3 +289,5 @@ export interface I18nSetup {
*/
Context: ({ children }: { children: React.ReactNode }) => JSX.Element;
}
export type I18nStart = I18nSetup;

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { I18nService, I18nSetup } from './i18n_service';
export { I18nService, I18nSetup, I18nStart } from './i18n_service';

View file

@ -18,14 +18,24 @@
*/
import { BasePathSetup } from './base_path';
import { Capabilities, CapabilitiesSetup } from './capabilities';
import { Capabilities, CapabilitiesStart } from './capabilities';
import { ChromeBrand, ChromeBreadcrumb, ChromeHelpExtension, ChromeSetup } from './chrome';
import { FatalErrorsSetup } from './fatal_errors';
import { HttpSetup } from './http';
import { I18nSetup } from './i18n';
import { InjectedMetadataParams, InjectedMetadataSetup } from './injected_metadata';
import { NotificationsSetup, Toast, ToastInput, ToastsSetup } from './notifications';
import { FlyoutRef, OverlaySetup } from './overlays';
import { I18nSetup, I18nStart } from './i18n';
import {
InjectedMetadataParams,
InjectedMetadataSetup,
InjectedMetadataStart,
} from './injected_metadata';
import {
NotificationsSetup,
Toast,
ToastInput,
ToastsApi,
NotificationsStart,
} from './notifications';
import { FlyoutRef, OverlayStart } from './overlays';
import { Plugin, PluginInitializer, PluginInitializerContext, PluginSetupContext } from './plugins';
import { UiSettingsClient, UiSettingsSetup, UiSettingsState } from './ui_settings';
@ -53,39 +63,51 @@ export interface CoreSetup {
http: HttpSetup;
/** {@link BasePathSetup} */
basePath: BasePathSetup;
/** {@link CapabilitiesSetup} */
capabilities: CapabilitiesSetup;
/** {@link UiSettingsSetup} */
uiSettings: UiSettingsSetup;
/** {@link ChromeSetup} */
chrome: ChromeSetup;
/** {@link OverlaySetup} */
overlays: OverlaySetup;
}
export interface CoreStart {
/** {@link CapabilitiesStart} */
capabilities: CapabilitiesStart;
/** {@link I18nStart} */
i18n: I18nStart;
/** {@link InjectedMetadataStart} */
injectedMetadata: InjectedMetadataStart;
/** {@link NotificationsStart} */
notifications: NotificationsStart;
/** {@link OverlayStart} */
overlays: OverlayStart;
}
export {
BasePathSetup,
HttpSetup,
FatalErrorsSetup,
CapabilitiesSetup,
Capabilities,
CapabilitiesStart,
ChromeSetup,
ChromeBreadcrumb,
ChromeBrand,
ChromeHelpExtension,
I18nSetup,
I18nStart,
InjectedMetadataSetup,
InjectedMetadataStart,
InjectedMetadataParams,
Plugin,
PluginInitializer,
PluginInitializerContext,
PluginSetupContext,
NotificationsSetup,
OverlaySetup,
NotificationsStart,
OverlayStart,
FlyoutRef,
Toast,
ToastInput,
ToastsSetup,
ToastsApi,
UiSettingsClient,
UiSettingsState,
UiSettingsSetup,

View file

@ -21,4 +21,5 @@ export {
InjectedMetadataService,
InjectedMetadataParams,
InjectedMetadataSetup,
InjectedMetadataStart,
} from './injected_metadata_service';

View file

@ -40,18 +40,18 @@ const createSetupContractMock = () => {
return setupContract;
};
const createStartContractMock = createSetupContractMock;
type InjectedMetadataServiceContract = PublicMethodsOf<InjectedMetadataService>;
const createMock = () => {
const mocked: jest.Mocked<InjectedMetadataServiceContract> = {
setup: jest.fn(),
getKibanaVersion: jest.fn(),
getKibanaBuildNumber: jest.fn(),
};
mocked.setup.mockReturnValue(createSetupContractMock());
return mocked;
};
const createMock = (): jest.Mocked<InjectedMetadataServiceContract> => ({
setup: jest.fn().mockReturnValue(createSetupContractMock()),
start: jest.fn().mockReturnValue(createStartContractMock()),
getKibanaVersion: jest.fn(),
getKibanaBuildNumber: jest.fn(),
});
export const injectedMetadataServiceMock = {
create: createMock,
createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
};

View file

@ -105,6 +105,10 @@ export class InjectedMetadataService {
};
}
public start(): InjectedMetadataStart {
return this.setup();
}
public getKibanaVersion() {
return this.state.version;
}
@ -154,3 +158,6 @@ export interface InjectedMetadataSetup {
[key: string]: unknown;
};
}
/** @public */
export type InjectedMetadataStart = InjectedMetadataSetup;

View file

@ -6,7 +6,6 @@ Array [
"ui/i18n",
"ui/notify/fatal_error",
"ui/notify/toasts",
"ui/capabilities",
"ui/chrome/api/loading_count",
"ui/chrome/api/base_path",
"ui/chrome/api/ui_settings",
@ -27,7 +26,6 @@ Array [
"ui/i18n",
"ui/notify/fatal_error",
"ui/notify/toasts",
"ui/capabilities",
"ui/chrome/api/loading_count",
"ui/chrome/api/base_path",
"ui/chrome/api/ui_settings",
@ -42,6 +40,48 @@ Array [
]
`;
exports[`#start() load order useLegacyTestHarness = false loads ui/modules before ui/chrome, and both before legacy files 1`] = `
Array [
"ui/metadata",
"ui/i18n",
"ui/notify/fatal_error",
"ui/notify/toasts",
"ui/chrome/api/loading_count",
"ui/chrome/api/base_path",
"ui/chrome/api/ui_settings",
"ui/chrome/api/injected_vars",
"ui/chrome/api/controls",
"ui/chrome/api/help_extension",
"ui/chrome/api/theme",
"ui/chrome/api/breadcrumbs",
"ui/chrome/services/global_nav_state",
"ui/chrome",
"legacy files",
"ui/capabilities",
]
`;
exports[`#start() load order useLegacyTestHarness = true loads ui/modules before ui/test_harness, and both before legacy files 1`] = `
Array [
"ui/metadata",
"ui/i18n",
"ui/notify/fatal_error",
"ui/notify/toasts",
"ui/chrome/api/loading_count",
"ui/chrome/api/base_path",
"ui/chrome/api/ui_settings",
"ui/chrome/api/injected_vars",
"ui/chrome/api/controls",
"ui/chrome/api/help_extension",
"ui/chrome/api/theme",
"ui/chrome/api/breadcrumbs",
"ui/chrome/services/global_nav_state",
"ui/test_harness",
"legacy files",
"ui/capabilities",
]
`;
exports[`#stop() destroys the angular scope and empties the targetDomElement if angular is bootstrapped to targetDomElement 1`] = `
<div
class="ng-scope"

View file

@ -22,6 +22,7 @@ type LegacyPlatformServiceContract = PublicMethodsOf<LegacyPlatformService>;
const createMock = () => {
const mocked: jest.Mocked<LegacyPlatformServiceContract> = {
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
};
return mocked;

View file

@ -25,7 +25,7 @@ const mockUiMetadataInit = jest.fn();
jest.mock('ui/metadata', () => {
mockLoadOrder.push('ui/metadata');
return {
__newPlatformInit__: mockUiMetadataInit,
__newPlatformSetup__: mockUiMetadataInit,
};
});
@ -49,7 +49,7 @@ const mockI18nContextInit = jest.fn();
jest.mock('ui/i18n', () => {
mockLoadOrder.push('ui/i18n');
return {
__newPlatformInit__: mockI18nContextInit,
__newPlatformSetup__: mockI18nContextInit,
};
});
@ -57,7 +57,7 @@ const mockUICapabilitiesInit = jest.fn();
jest.mock('ui/capabilities', () => {
mockLoadOrder.push('ui/capabilities');
return {
__newPlatformInit__: mockUICapabilitiesInit,
__newPlatformStart__: mockUICapabilitiesInit,
};
});
@ -65,7 +65,7 @@ const mockFatalErrorInit = jest.fn();
jest.mock('ui/notify/fatal_error', () => {
mockLoadOrder.push('ui/notify/fatal_error');
return {
__newPlatformInit__: mockFatalErrorInit,
__newPlatformSetup__: mockFatalErrorInit,
};
});
@ -73,7 +73,7 @@ const mockNotifyToastsInit = jest.fn();
jest.mock('ui/notify/toasts', () => {
mockLoadOrder.push('ui/notify/toasts');
return {
__newPlatformInit__: mockNotifyToastsInit,
__newPlatformSetup__: mockNotifyToastsInit,
};
});
@ -81,7 +81,7 @@ const mockHttpInit = jest.fn();
jest.mock('ui/chrome/api/loading_count', () => {
mockLoadOrder.push('ui/chrome/api/loading_count');
return {
__newPlatformInit__: mockHttpInit,
__newPlatformSetup__: mockHttpInit,
};
});
@ -89,7 +89,7 @@ const mockBasePathInit = jest.fn();
jest.mock('ui/chrome/api/base_path', () => {
mockLoadOrder.push('ui/chrome/api/base_path');
return {
__newPlatformInit__: mockBasePathInit,
__newPlatformSetup__: mockBasePathInit,
};
});
@ -97,7 +97,7 @@ const mockUiSettingsInit = jest.fn();
jest.mock('ui/chrome/api/ui_settings', () => {
mockLoadOrder.push('ui/chrome/api/ui_settings');
return {
__newPlatformInit__: mockUiSettingsInit,
__newPlatformSetup__: mockUiSettingsInit,
};
});
@ -105,7 +105,7 @@ const mockInjectedVarsInit = jest.fn();
jest.mock('ui/chrome/api/injected_vars', () => {
mockLoadOrder.push('ui/chrome/api/injected_vars');
return {
__newPlatformInit__: mockInjectedVarsInit,
__newPlatformSetup__: mockInjectedVarsInit,
};
});
@ -113,7 +113,7 @@ const mockChromeControlsInit = jest.fn();
jest.mock('ui/chrome/api/controls', () => {
mockLoadOrder.push('ui/chrome/api/controls');
return {
__newPlatformInit__: mockChromeControlsInit,
__newPlatformSetup__: mockChromeControlsInit,
};
});
@ -121,7 +121,7 @@ const mockChromeHelpExtensionInit = jest.fn();
jest.mock('ui/chrome/api/help_extension', () => {
mockLoadOrder.push('ui/chrome/api/help_extension');
return {
__newPlatformInit__: mockChromeHelpExtensionInit,
__newPlatformSetup__: mockChromeHelpExtensionInit,
};
});
@ -129,7 +129,7 @@ const mockChromeThemeInit = jest.fn();
jest.mock('ui/chrome/api/theme', () => {
mockLoadOrder.push('ui/chrome/api/theme');
return {
__newPlatformInit__: mockChromeThemeInit,
__newPlatformSetup__: mockChromeThemeInit,
};
});
@ -137,7 +137,7 @@ const mockChromeBreadcrumbsInit = jest.fn();
jest.mock('ui/chrome/api/breadcrumbs', () => {
mockLoadOrder.push('ui/chrome/api/breadcrumbs');
return {
__newPlatformInit__: mockChromeBreadcrumbsInit,
__newPlatformSetup__: mockChromeBreadcrumbsInit,
};
});
@ -145,7 +145,7 @@ const mockGlobalNavStateInit = jest.fn();
jest.mock('ui/chrome/services/global_nav_state', () => {
mockLoadOrder.push('ui/chrome/services/global_nav_state');
return {
__newPlatformInit__: mockGlobalNavStateInit,
__newPlatformSetup__: mockGlobalNavStateInit,
};
});
@ -168,28 +168,42 @@ const httpSetup = httpServiceMock.createSetupContract();
const i18nSetup = i18nServiceMock.createSetupContract();
const injectedMetadataSetup = injectedMetadataServiceMock.createSetupContract();
const notificationsSetup = notificationServiceMock.createSetupContract();
const capabilitiesSetup = capabilitiesServiceMock.createSetupContract();
const uiSettingsSetup = uiSettingsServiceMock.createSetupContract();
const overlaySetup = overlayServiceMock.createSetupContract();
const defaultParams = {
targetDomElement: document.createElement('div'),
requireLegacyFiles: jest.fn(() => {
mockLoadOrder.push('legacy files');
}),
};
const defaultSetupDeps = {
i18n: i18nSetup,
fatalErrors: fatalErrorsSetup,
injectedMetadata: injectedMetadataSetup,
notifications: notificationsSetup,
http: httpSetup,
basePath: basePathSetup,
capabilities: capabilitiesSetup,
uiSettings: uiSettingsSetup,
chrome: chromeSetup,
overlays: overlaySetup,
core: {
i18n: i18nSetup,
fatalErrors: fatalErrorsSetup,
injectedMetadata: injectedMetadataSetup,
notifications: notificationsSetup,
http: httpSetup,
basePath: basePathSetup,
uiSettings: uiSettingsSetup,
chrome: chromeSetup,
},
};
const capabilitiesStart = capabilitiesServiceMock.createStartContract();
const i18nStart = i18nServiceMock.createStartContract();
const injectedMetadataStart = injectedMetadataServiceMock.createStartContract();
const notificationsStart = notificationServiceMock.createStartContract();
const overlayStart = overlayServiceMock.createStartContract();
const defaultStartDeps = {
core: {
capabilities: capabilitiesStart,
i18n: i18nStart,
injectedMetadata: injectedMetadataStart,
notifications: notificationsStart,
overlays: overlayStart,
},
targetDomElement: document.createElement('div'),
};
afterEach(() => {
@ -226,17 +240,6 @@ describe('#setup()', () => {
expect(mockI18nContextInit).toHaveBeenCalledWith(i18nSetup.Context);
});
it('passes uiCapabilities to ui/capabilities', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
});
legacyPlatform.setup(defaultSetupDeps);
expect(mockUICapabilitiesInit).toHaveBeenCalledTimes(1);
expect(mockUICapabilitiesInit).toHaveBeenCalledWith(capabilitiesSetup);
});
it('passes fatalErrors service to ui/notify/fatal_errors', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
@ -357,35 +360,6 @@ describe('#setup()', () => {
expect(mockGlobalNavStateInit).toHaveBeenCalledTimes(1);
expect(mockGlobalNavStateInit).toHaveBeenCalledWith(chromeSetup);
});
describe('useLegacyTestHarness = false', () => {
it('passes the targetDomElement to ui/chrome', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
});
legacyPlatform.setup(defaultSetupDeps);
expect(mockUiTestHarnessBootstrap).not.toHaveBeenCalled();
expect(mockUiChromeBootstrap).toHaveBeenCalledTimes(1);
expect(mockUiChromeBootstrap).toHaveBeenCalledWith(defaultParams.targetDomElement);
});
});
describe('useLegacyTestHarness = true', () => {
it('passes the targetDomElement to ui/test_harness', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
useLegacyTestHarness: true,
});
legacyPlatform.setup(defaultSetupDeps);
expect(mockUiChromeBootstrap).not.toHaveBeenCalled();
expect(mockUiTestHarnessBootstrap).toHaveBeenCalledTimes(1);
expect(mockUiTestHarnessBootstrap).toHaveBeenCalledWith(defaultParams.targetDomElement);
});
});
});
describe('load order', () => {
@ -420,6 +394,84 @@ describe('#setup()', () => {
});
});
describe('#start()', () => {
it('passes uiCapabilities to ui/capabilities', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
});
legacyPlatform.setup(defaultSetupDeps);
legacyPlatform.start(defaultStartDeps);
expect(mockUICapabilitiesInit).toHaveBeenCalledTimes(1);
expect(mockUICapabilitiesInit).toHaveBeenCalledWith(capabilitiesStart);
});
describe('useLegacyTestHarness = false', () => {
it('passes the targetDomElement to ui/chrome', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
});
legacyPlatform.setup(defaultSetupDeps);
legacyPlatform.start(defaultStartDeps);
expect(mockUiTestHarnessBootstrap).not.toHaveBeenCalled();
expect(mockUiChromeBootstrap).toHaveBeenCalledTimes(1);
expect(mockUiChromeBootstrap).toHaveBeenCalledWith(defaultStartDeps.targetDomElement);
});
});
describe('useLegacyTestHarness = true', () => {
it('passes the targetDomElement to ui/test_harness', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
useLegacyTestHarness: true,
});
legacyPlatform.setup(defaultSetupDeps);
legacyPlatform.start(defaultStartDeps);
expect(mockUiChromeBootstrap).not.toHaveBeenCalled();
expect(mockUiTestHarnessBootstrap).toHaveBeenCalledTimes(1);
expect(mockUiTestHarnessBootstrap).toHaveBeenCalledWith(defaultStartDeps.targetDomElement);
});
});
describe('load order', () => {
describe('useLegacyTestHarness = false', () => {
it('loads ui/modules before ui/chrome, and both before legacy files', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
});
expect(mockLoadOrder).toEqual([]);
legacyPlatform.setup(defaultSetupDeps);
legacyPlatform.start(defaultStartDeps);
expect(mockLoadOrder).toMatchSnapshot();
});
});
describe('useLegacyTestHarness = true', () => {
it('loads ui/modules before ui/test_harness, and both before legacy files', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
useLegacyTestHarness: true,
});
expect(mockLoadOrder).toEqual([]);
legacyPlatform.setup(defaultSetupDeps);
legacyPlatform.start(defaultStartDeps);
expect(mockLoadOrder).toMatchSnapshot();
});
});
});
});
describe('#stop()', () => {
it('does nothing if angular was not bootstrapped to targetDomElement', () => {
const targetDomElement = document.createElement('div');
@ -429,20 +481,18 @@ describe('#stop()', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
targetDomElement,
});
legacyPlatform.stop();
expect(targetDomElement).toMatchSnapshot();
});
it('destroys the angular scope and empties the targetDomElement if angular is bootstrapped to targetDomElement', () => {
it('destroys the angular scope and empties the targetDomElement if angular is bootstrapped to targetDomElement', async () => {
const targetDomElement = document.createElement('div');
const scopeDestroySpy = jest.fn();
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
targetDomElement,
});
// simulate bootstrapping with a module "foo"
@ -459,6 +509,8 @@ describe('#stop()', () => {
angular.bootstrap(targetDomElement, ['foo']);
await legacyPlatform.setup(defaultSetupDeps);
legacyPlatform.start({ ...defaultStartDeps, targetDomElement });
legacyPlatform.stop();
expect(targetDomElement).toMatchSnapshot();

View file

@ -18,14 +18,27 @@
*/
import angular from 'angular';
import { CoreSetup } from '../';
import { CoreSetup, CoreStart } from '../';
/** @internal */
export interface LegacyPlatformParams {
targetDomElement: HTMLElement;
requireLegacyFiles: () => void;
useLegacyTestHarness?: boolean;
}
interface SetupDeps {
core: CoreSetup;
}
interface StartDeps {
core: CoreStart;
targetDomElement: HTMLElement;
}
interface BootstrapModule {
bootstrap: (targetDomElement: HTMLElement) => void;
}
/**
* The LegacyPlatformService is responsible for initializing
* the legacy platform by injecting parts of the new platform
@ -34,9 +47,12 @@ export interface LegacyPlatformParams {
* setup either the app or browser tests.
*/
export class LegacyPlatformService {
private bootstrapModule?: BootstrapModule;
private targetDomElement?: HTMLElement;
constructor(private readonly params: LegacyPlatformParams) {}
public setup(core: CoreSetup) {
public async setup({ core }: SetupDeps) {
const {
i18n,
injectedMetadata,
@ -44,40 +60,53 @@ export class LegacyPlatformService {
notifications,
http,
basePath,
capabilities,
uiSettings,
chrome,
} = core;
// Inject parts of the new platform into parts of the legacy platform
// so that legacy APIs/modules can mimic their new platform counterparts
require('ui/new_platform').__newPlatformInit__(core);
require('ui/metadata').__newPlatformInit__(injectedMetadata.getLegacyMetadata());
require('ui/i18n').__newPlatformInit__(i18n.Context);
require('ui/notify/fatal_error').__newPlatformInit__(fatalErrors);
require('ui/notify/toasts').__newPlatformInit__(notifications.toasts);
require('ui/capabilities').__newPlatformInit__(capabilities);
require('ui/chrome/api/loading_count').__newPlatformInit__(http);
require('ui/chrome/api/base_path').__newPlatformInit__(basePath);
require('ui/chrome/api/ui_settings').__newPlatformInit__(uiSettings);
require('ui/chrome/api/injected_vars').__newPlatformInit__(injectedMetadata);
require('ui/chrome/api/controls').__newPlatformInit__(chrome);
require('ui/chrome/api/help_extension').__newPlatformInit__(chrome);
require('ui/chrome/api/theme').__newPlatformInit__(chrome);
require('ui/chrome/api/breadcrumbs').__newPlatformInit__(chrome);
require('ui/chrome/services/global_nav_state').__newPlatformInit__(chrome);
require('ui/new_platform').__newPlatformSetup__(core);
require('ui/metadata').__newPlatformSetup__(injectedMetadata.getLegacyMetadata());
require('ui/i18n').__newPlatformSetup__(i18n.Context);
require('ui/notify/fatal_error').__newPlatformSetup__(fatalErrors);
require('ui/notify/toasts').__newPlatformSetup__(notifications.toasts);
require('ui/chrome/api/loading_count').__newPlatformSetup__(http);
require('ui/chrome/api/base_path').__newPlatformSetup__(basePath);
require('ui/chrome/api/ui_settings').__newPlatformSetup__(uiSettings);
require('ui/chrome/api/injected_vars').__newPlatformSetup__(injectedMetadata);
require('ui/chrome/api/controls').__newPlatformSetup__(chrome);
require('ui/chrome/api/help_extension').__newPlatformSetup__(chrome);
require('ui/chrome/api/theme').__newPlatformSetup__(chrome);
require('ui/chrome/api/breadcrumbs').__newPlatformSetup__(chrome);
require('ui/chrome/services/global_nav_state').__newPlatformSetup__(chrome);
// Load the bootstrap module before loading the legacy platform files so that
// the bootstrap module can modify the environment a bit first
const bootstrapModule = this.loadBootstrapModule();
this.bootstrapModule = this.loadBootstrapModule();
// require the files that will tie into the legacy platform
this.params.requireLegacyFiles();
}
bootstrapModule.bootstrap(this.params.targetDomElement);
public start({ core, targetDomElement }: StartDeps) {
if (!this.bootstrapModule) {
throw new Error('Bootstrap module must be loaded before `start`');
}
this.targetDomElement = targetDomElement;
require('ui/new_platform').__newPlatformStart__(core);
require('ui/capabilities').__newPlatformStart__(core.capabilities);
this.bootstrapModule.bootstrap(this.targetDomElement);
}
public stop() {
const angularRoot = angular.element(this.params.targetDomElement);
if (!this.targetDomElement) {
return;
}
const angularRoot = angular.element(this.targetDomElement);
const injector$ = angularRoot.injector();
// if we haven't gotten to the point of bootstraping
@ -90,12 +119,10 @@ export class LegacyPlatformService {
injector$.get('$rootScope').$destroy();
// clear the inner html of the root angular element
this.params.targetDomElement.textContent = '';
this.targetDomElement.textContent = '';
}
private loadBootstrapModule(): {
bootstrap: (targetDomElement: HTMLElement) => void;
} {
private loadBootstrapModule(): BootstrapModule {
if (this.params.useLegacyTestHarness) {
// wrapped in NODE_ENV check so the `ui/test_harness` module
// is not included in the distributable

View file

@ -17,5 +17,9 @@
* under the License.
*/
export { Toast, ToastInput, ToastsSetup } from './toasts';
export { NotificationsService, NotificationsSetup } from './notifications_service';
export { Toast, ToastInput, ToastsApi } from './toasts';
export {
NotificationsService,
NotificationsSetup,
NotificationsStart,
} from './notifications_service';

View file

@ -16,22 +16,35 @@
* specific language governing permissions and limitations
* under the License.
*/
import { NotificationsService, NotificationsSetup } from './notifications_service';
import {
NotificationsService,
NotificationsSetup,
NotificationsStart,
} from './notifications_service';
import { toastsServiceMock } from './toasts/toasts_service.mock';
import { ToastsSetup } from './toasts/toasts_start';
import { ToastsApi } from './toasts/toasts_api';
const createSetupContractMock = () => {
const setupContract: jest.Mocked<NotificationsSetup> = {
// we have to suppress type errors until decide how to mock es6 class
toasts: (toastsServiceMock.createSetupContract() as unknown) as ToastsSetup,
toasts: (toastsServiceMock.createSetupContract() as unknown) as ToastsApi,
};
return setupContract;
};
const createStartContractMock = () => {
const startContract: jest.Mocked<NotificationsStart> = {
// we have to suppress type errors until decide how to mock es6 class
toasts: (toastsServiceMock.createStartContract() as unknown) as ToastsApi,
};
return startContract;
};
type NotificationsServiceContract = PublicMethodsOf<NotificationsService>;
const createMock = () => {
const mocked: jest.Mocked<NotificationsServiceContract> = {
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
};
mocked.setup.mockReturnValue(createSetupContractMock());
@ -41,4 +54,5 @@ const createMock = () => {
export const notificationServiceMock = {
create: createMock,
createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
};

View file

@ -19,49 +19,33 @@
import { i18n } from '@kbn/i18n';
import { Observable, Subject, Subscription } from 'rxjs';
import { I18nSetup } from '../i18n';
import { Subscription } from 'rxjs';
import { I18nStart } from '../i18n';
import { ToastsService } from './toasts';
import { ToastsApi } from './toasts/toasts_api';
import { UiSettingsSetup } from '../ui_settings';
interface NotificationServiceParams {
targetDomElement$: Observable<HTMLElement>;
interface SetupDeps {
uiSettings: UiSettingsSetup;
}
interface NotificationsServiceDeps {
i18n: I18nSetup;
uiSettings: UiSettingsSetup;
interface StartDeps {
i18n: I18nStart;
targetDomElement: HTMLElement;
}
/** @public */
export class NotificationsService {
private readonly toasts: ToastsService;
private readonly toastsContainer$: Subject<HTMLElement>;
private domElemSubscription?: Subscription;
private uiSettingsErrorSubscription?: Subscription;
private targetDomElement?: HTMLElement;
constructor(private readonly params: NotificationServiceParams) {
this.toastsContainer$ = new Subject<HTMLElement>();
this.toasts = new ToastsService({
targetDomElement$: this.toastsContainer$.asObservable(),
});
constructor() {
this.toasts = new ToastsService();
}
public setup({ i18n: i18nDep, uiSettings }: NotificationsServiceDeps) {
this.domElemSubscription = this.params.targetDomElement$.subscribe({
next: targetDomElement => {
this.cleanupTargetDomElement();
this.targetDomElement = targetDomElement;
const toastsContainer = document.createElement('div');
targetDomElement.appendChild(toastsContainer);
this.toastsContainer$.next(toastsContainer);
},
});
const notificationSetup = { toasts: this.toasts.setup({ i18n: i18nDep }) };
public setup({ uiSettings }: SetupDeps): NotificationsSetup {
const notificationSetup = { toasts: this.toasts.setup() };
this.uiSettingsErrorSubscription = uiSettings.getUpdateErrors$().subscribe(error => {
notificationSetup.toasts.addDanger({
@ -75,25 +59,31 @@ export class NotificationsService {
return notificationSetup;
}
public start({ i18n: i18nDep, targetDomElement }: StartDeps): NotificationsStart {
this.targetDomElement = targetDomElement;
const toastsContainer = document.createElement('div');
targetDomElement.appendChild(toastsContainer);
return { toasts: this.toasts.start({ i18n: i18nDep, targetDomElement: toastsContainer }) };
}
public stop() {
this.toasts.stop();
this.cleanupTargetDomElement();
if (this.domElemSubscription) {
this.domElemSubscription.unsubscribe();
if (this.targetDomElement) {
this.targetDomElement.textContent = '';
}
if (this.uiSettingsErrorSubscription) {
this.uiSettingsErrorSubscription.unsubscribe();
}
}
private cleanupTargetDomElement() {
if (this.targetDomElement) {
this.targetDomElement.textContent = '';
}
}
}
/** @public */
export type NotificationsSetup = ReturnType<NotificationsService['setup']>;
export interface NotificationsSetup {
toasts: ToastsApi;
}
/** @public */
export type NotificationsStart = NotificationsSetup;

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`#setup() renders the GlobalToastList into the targetDomElement param 1`] = `
exports[`#start() renders the GlobalToastList into the targetDomElement param 1`] = `
Array [
Array [
<I18nContext>

View file

@ -18,5 +18,5 @@
*/
export { ToastsService } from './toasts_service';
export { ToastsSetup, ToastInput } from './toasts_start';
export { ToastsApi, ToastInput } from './toasts_api';
export { Toast } from '@elastic/eui';

View file

@ -19,9 +19,9 @@
import { take } from 'rxjs/operators';
import { ToastsSetup } from './toasts_start';
import { ToastsApi } from './toasts_api';
async function getCurrentToasts(toasts: ToastsSetup) {
async function getCurrentToasts(toasts: ToastsApi) {
return await toasts
.get$()
.pipe(take(1))
@ -30,7 +30,7 @@ async function getCurrentToasts(toasts: ToastsSetup) {
describe('#get$()', () => {
it('returns observable that emits NEW toast list when something added or removed', () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
const onToasts = jest.fn();
toasts.get$().subscribe(onToasts);
@ -57,7 +57,7 @@ describe('#get$()', () => {
});
it('does not emit a new toast list when unknown toast is passed to remove()', () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
const onToasts = jest.fn();
toasts.get$().subscribe(onToasts);
@ -71,14 +71,14 @@ describe('#get$()', () => {
describe('#add()', () => {
it('returns toast objects with auto assigned id', () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
const toast = toasts.add({ title: 'foo' });
expect(toast).toHaveProperty('id');
expect(toast).toHaveProperty('title', 'foo');
});
it('adds the toast to toasts list', async () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
const toast = toasts.add({});
const currentToasts = await getCurrentToasts(toasts);
@ -87,27 +87,27 @@ describe('#add()', () => {
});
it('increments the toast ID for each additional toast', () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
expect(toasts.add({})).toHaveProperty('id', '0');
expect(toasts.add({})).toHaveProperty('id', '1');
expect(toasts.add({})).toHaveProperty('id', '2');
});
it('accepts a string, uses it as the title', async () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
expect(toasts.add('foo')).toHaveProperty('title', 'foo');
});
});
describe('#remove()', () => {
it('removes a toast', async () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
toasts.remove(toasts.add('Test'));
expect(await getCurrentToasts(toasts)).toHaveLength(0);
});
it('ignores unknown toast', async () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
toasts.add('Test');
toasts.remove({ id: 'foo' });
@ -118,12 +118,12 @@ describe('#remove()', () => {
describe('#addSuccess()', () => {
it('adds a success toast', async () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
expect(toasts.addSuccess({})).toHaveProperty('color', 'success');
});
it('returns the created toast', async () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
const toast = toasts.addSuccess({});
const currentToasts = await getCurrentToasts(toasts);
expect(currentToasts[0]).toBe(toast);
@ -132,12 +132,12 @@ describe('#addSuccess()', () => {
describe('#addWarning()', () => {
it('adds a warning toast', async () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
expect(toasts.addWarning({})).toHaveProperty('color', 'warning');
});
it('returns the created toast', async () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
const toast = toasts.addWarning({});
const currentToasts = await getCurrentToasts(toasts);
expect(currentToasts[0]).toBe(toast);
@ -146,12 +146,12 @@ describe('#addWarning()', () => {
describe('#addDanger()', () => {
it('adds a danger toast', async () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
expect(toasts.addDanger({})).toHaveProperty('color', 'danger');
});
it('returns the created toast', async () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
const toast = toasts.addDanger({});
const currentToasts = await getCurrentToasts(toasts);
expect(currentToasts[0]).toBe(toast);

View file

@ -34,7 +34,7 @@ const normalizeToast = (toastOrTitle: ToastInput) => {
};
/** @public */
export class ToastsSetup {
export class ToastsApi {
private toasts$ = new Rx.BehaviorSubject<Toast[]>([]);
private idCounter = 0;

View file

@ -16,10 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ToastsSetup } from './toasts_start';
import { ToastsApi } from './toasts_api';
const createSetupContractMock = () => {
const setupContract: jest.Mocked<PublicMethodsOf<ToastsSetup>> = {
const createToastsApiMock = () => {
const api: jest.Mocked<PublicMethodsOf<ToastsApi>> = {
get$: jest.fn(),
add: jest.fn(),
remove: jest.fn(),
@ -27,9 +27,14 @@ const createSetupContractMock = () => {
addWarning: jest.fn(),
addDanger: jest.fn(),
};
return setupContract;
return api;
};
const createSetupContractMock = createToastsApiMock;
const createStartContractMock = createToastsApiMock;
export const toastsServiceMock = {
createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
};

View file

@ -19,9 +19,8 @@
import { mockReactDomRender, mockReactDomUnmount } from './toasts_service.test.mocks';
import { of } from 'rxjs';
import { ToastsService } from './toasts_service';
import { ToastsSetup } from './toasts_start';
import { ToastsApi } from './toasts_api';
const mockI18n: any = {
Context: function I18nContext() {
@ -30,22 +29,31 @@ const mockI18n: any = {
};
describe('#setup()', () => {
it('returns a ToastsApi', () => {
const toasts = new ToastsService();
expect(toasts.setup()).toBeInstanceOf(ToastsApi);
});
});
describe('#start()', () => {
it('renders the GlobalToastList into the targetDomElement param', async () => {
const targetDomElement = document.createElement('div');
targetDomElement.setAttribute('test', 'target-dom-element');
const toasts = new ToastsService({ targetDomElement$: of(targetDomElement) });
const toasts = new ToastsService();
expect(mockReactDomRender).not.toHaveBeenCalled();
toasts.setup({ i18n: mockI18n });
toasts.setup();
toasts.start({ i18n: mockI18n, targetDomElement });
expect(mockReactDomRender.mock.calls).toMatchSnapshot();
});
it('returns a ToastsSetup', () => {
const toasts = new ToastsService({
targetDomElement$: of(document.createElement('div')),
});
it('returns a ToastsApi', () => {
const targetDomElement = document.createElement('div');
const toasts = new ToastsService();
expect(toasts.setup({ i18n: mockI18n })).toBeInstanceOf(ToastsSetup);
toasts.setup();
expect(toasts.start({ i18n: mockI18n, targetDomElement })).toBeInstanceOf(ToastsApi);
});
});
@ -53,9 +61,10 @@ describe('#stop()', () => {
it('unmounts the GlobalToastList from the targetDomElement', () => {
const targetDomElement = document.createElement('div');
targetDomElement.setAttribute('test', 'target-dom-element');
const toasts = new ToastsService({ targetDomElement$: of(targetDomElement) });
const toasts = new ToastsService();
toasts.setup({ i18n: mockI18n });
toasts.setup();
toasts.start({ i18n: mockI18n, targetDomElement });
expect(mockReactDomUnmount).not.toHaveBeenCalled();
toasts.stop();
@ -63,9 +72,7 @@ describe('#stop()', () => {
});
it('does not fail if setup() was never called', () => {
const targetDomElement = document.createElement('div');
targetDomElement.setAttribute('test', 'target-dom-element');
const toasts = new ToastsService({ targetDomElement$: of(targetDomElement) });
const toasts = new ToastsService();
expect(() => {
toasts.stop();
}).not.toThrowError();
@ -73,9 +80,10 @@ describe('#stop()', () => {
it('empties the content of the targetDomElement', () => {
const targetDomElement = document.createElement('div');
const toasts = new ToastsService({ targetDomElement$: of(targetDomElement) });
const toasts = new ToastsService();
toasts.setup({ i18n: mockI18n });
toasts.setup();
toasts.start({ i18n: mockI18n, targetDomElement });
toasts.stop();
expect(targetDomElement.childNodes).toHaveLength(0);
});

View file

@ -19,59 +19,43 @@
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { Observable, Subscription } from 'rxjs';
import { Toast } from '@elastic/eui';
import { I18nSetup } from '../../i18n';
import { GlobalToastList } from './global_toast_list';
import { ToastsSetup } from './toasts_start';
import { ToastsApi } from './toasts_api';
interface Params {
targetDomElement$: Observable<HTMLElement>;
}
interface Deps {
interface StartDeps {
i18n: I18nSetup;
targetDomElement: HTMLElement;
}
export class ToastsService {
private domElemSubscription?: Subscription;
private api?: ToastsApi;
private targetDomElement?: HTMLElement;
constructor(private readonly params: Params) {}
public setup() {
this.api = new ToastsApi();
return this.api!;
}
public setup({ i18n }: Deps) {
const toasts = new ToastsSetup();
public start({ i18n, targetDomElement }: StartDeps) {
this.targetDomElement = targetDomElement;
this.domElemSubscription = this.params.targetDomElement$.subscribe({
next: targetDomElement => {
this.cleanupTargetDomElement();
this.targetDomElement = targetDomElement;
render(
<i18n.Context>
<GlobalToastList
dismissToast={(toast: Toast) => this.api!.remove(toast)}
toasts$={this.api!.get$()}
/>
</i18n.Context>,
targetDomElement
);
render(
<i18n.Context>
<GlobalToastList
dismissToast={(toast: Toast) => toasts.remove(toast)}
toasts$={toasts.get$()}
/>
</i18n.Context>,
targetDomElement
);
},
});
return toasts;
return this.api!;
}
public stop() {
this.cleanupTargetDomElement();
if (this.domElemSubscription) {
this.domElemSubscription.unsubscribe();
}
}
private cleanupTargetDomElement() {
if (this.targetDomElement) {
unmountComponentAtNode(this.targetDomElement);
this.targetDomElement.textContent = '';

View file

@ -17,5 +17,5 @@
* under the License.
*/
export { OverlayService, OverlaySetup } from './overlay_service';
export { OverlayService, OverlayStart } from './overlay_service';
export { FlyoutRef } from './flyout';

View file

@ -16,24 +16,24 @@
* specific language governing permissions and limitations
* under the License.
*/
import { OverlayService, OverlaySetup } from './overlay_service';
import { OverlayService, OverlayStart } from './overlay_service';
const createSetupContractMock = () => {
const setupContract: jest.Mocked<PublicMethodsOf<OverlaySetup>> = {
const createStartContractMock = () => {
const startContract: jest.Mocked<PublicMethodsOf<OverlayStart>> = {
openFlyout: jest.fn(),
};
return setupContract;
return startContract;
};
const createMock = () => {
const mocked: jest.Mocked<PublicMethodsOf<OverlayService>> = {
setup: jest.fn(),
start: jest.fn(),
};
mocked.setup.mockReturnValue(createSetupContractMock());
mocked.start.mockReturnValue(createStartContractMock());
return mocked;
};
export const overlayServiceMock = {
create: createMock,
createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
};

View file

@ -20,10 +20,10 @@
import { FlyoutService } from './flyout';
import { FlyoutRef } from '..';
import { I18nSetup } from '../i18n';
import { I18nStart } from '../i18n';
interface Deps {
i18n: I18nSetup;
interface StartDeps {
i18n: I18nStart;
}
/** @internal */
@ -34,7 +34,7 @@ export class OverlayService {
this.flyoutService = new FlyoutService(targetDomElement);
}
public setup({ i18n }: Deps): OverlaySetup {
public start({ i18n }: StartDeps): OverlayStart {
return {
openFlyout: this.flyoutService.openFlyout.bind(this.flyoutService, i18n),
};
@ -42,7 +42,7 @@ export class OverlayService {
}
/** @public */
export interface OverlaySetup {
export interface OverlayStart {
openFlyout: (
flyoutChildren: React.ReactNode,
flyoutProps?: {

View file

@ -19,6 +19,7 @@
export const mockPlugin = {
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
};
export const mockInitializer = jest.fn(() => mockPlugin);

View file

@ -41,6 +41,7 @@ const addBasePath = (path: string) => path;
beforeEach(() => {
mockPluginLoader.mockClear();
mockPlugin.setup.mockClear();
mockPlugin.start.mockClear();
mockPlugin.stop.mockClear();
plugin = new PluginWrapper(createManifest('plugin-a'), initializerContext);
});
@ -58,13 +59,21 @@ describe('PluginWrapper', () => {
});
test('`setup` fails if plugin.setup is not a function', async () => {
mockInitializer.mockReturnValueOnce({ stop: jest.fn() } as any);
mockInitializer.mockReturnValueOnce({ start: jest.fn() } as any);
await plugin.load(addBasePath);
await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot(
`"Instance of plugin \\"plugin-a\\" does not define \\"setup\\" function."`
);
});
test('`setup` fails if plugin.start is not a function', async () => {
mockInitializer.mockReturnValueOnce({ setup: jest.fn() } as any);
await plugin.load(addBasePath);
await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot(
`"Instance of plugin \\"plugin-a\\" does not define \\"start\\" function."`
);
});
test('`setup` calls initializer with initializer context', async () => {
await plugin.load(addBasePath);
await plugin.setup({} as any, {} as any);
@ -79,6 +88,22 @@ describe('PluginWrapper', () => {
expect(mockPlugin.setup).toHaveBeenCalledWith(context, deps);
});
test('`start` fails if setup is not called first', async () => {
await plugin.load(addBasePath);
await expect(plugin.start({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot(
`"Plugin \\"plugin-a\\" can't be started since it isn't set up."`
);
});
test('`start` calls plugin.start with context and dependencies', async () => {
await plugin.load(addBasePath);
await plugin.setup({} as any, {} as any);
const context = { any: 'thing' } as any;
const deps = { otherDep: 'value' };
await plugin.start(context, deps);
expect(mockPlugin.start).toHaveBeenCalledWith(context, deps);
});
test('`stop` fails if plugin is not setup up', async () => {
expect(() => plugin.stop()).toThrowErrorMatchingInlineSnapshot(
`"Plugin \\"plugin-a\\" can't be stopped since it isn't set up."`
@ -93,7 +118,7 @@ describe('PluginWrapper', () => {
});
test('`stop` does not fail if plugin.stop does not exist', async () => {
mockInitializer.mockReturnValueOnce({ setup: jest.fn() } as any);
mockInitializer.mockReturnValueOnce({ setup: jest.fn(), start: jest.fn() } as any);
await plugin.load(addBasePath);
await plugin.setup({} as any, {} as any);
expect(() => plugin.stop()).not.toThrow();

View file

@ -18,7 +18,7 @@
*/
import { DiscoveredPlugin, PluginName } from '../../server';
import { PluginInitializerContext, PluginSetupContext } from './plugin_context';
import { PluginInitializerContext, PluginSetupContext, PluginStartContext } from './plugin_context';
import { loadPluginBundle } from './plugin_loader';
/**
@ -26,8 +26,14 @@ import { loadPluginBundle } from './plugin_loader';
*
* @public
*/
export interface Plugin<TSetup, TPluginsSetup extends Record<string, unknown> = {}> {
export interface Plugin<
TSetup,
TStart,
TPluginsSetup extends Record<string, unknown> = {},
TPluginsStart extends Record<string, unknown> = {}
> {
setup: (core: PluginSetupContext, plugins: TPluginsSetup) => TSetup | Promise<TSetup>;
start: (core: PluginStartContext, plugins: TPluginsStart) => TStart | Promise<TStart>;
stop?: () => void;
}
@ -37,9 +43,12 @@ export interface Plugin<TSetup, TPluginsSetup extends Record<string, unknown> =
*
* @public
*/
export type PluginInitializer<TSetup, TPluginsSetup extends Record<string, unknown> = {}> = (
core: PluginInitializerContext
) => Plugin<TSetup, TPluginsSetup>;
export type PluginInitializer<
TSetup,
TStart,
TPluginsSetup extends Record<string, unknown> = {},
TPluginsStart extends Record<string, unknown> = {}
> = (core: PluginInitializerContext) => Plugin<TSetup, TStart, TPluginsSetup, TPluginsStart>;
/**
* Lightweight wrapper around discovered plugin that is responsible for instantiating
@ -49,14 +58,16 @@ export type PluginInitializer<TSetup, TPluginsSetup extends Record<string, unkno
*/
export class PluginWrapper<
TSetup = unknown,
TPluginsSetup extends Record<PluginName, unknown> = Record<PluginName, unknown>
TStart = unknown,
TPluginsSetup extends Record<PluginName, unknown> = Record<PluginName, unknown>,
TPluginsStart extends Record<PluginName, unknown> = Record<PluginName, unknown>
> {
public readonly name: DiscoveredPlugin['id'];
public readonly configPath: DiscoveredPlugin['configPath'];
public readonly requiredPlugins: DiscoveredPlugin['requiredPlugins'];
public readonly optionalPlugins: DiscoveredPlugin['optionalPlugins'];
private initializer?: PluginInitializer<TSetup, TPluginsSetup>;
private instance?: Plugin<TSetup, TPluginsSetup>;
private initializer?: PluginInitializer<TSetup, TStart, TPluginsSetup, TPluginsStart>;
private instance?: Plugin<TSetup, TStart, TPluginsSetup, TPluginsStart>;
constructor(
readonly discoveredPlugin: DiscoveredPlugin,
@ -74,7 +85,10 @@ export class PluginWrapper<
* @param addBasePath Function that adds the base path to a string for plugin bundle path.
*/
public async load(addBasePath: (path: string) => string) {
this.initializer = await loadPluginBundle<TSetup, TPluginsSetup>(addBasePath, this.name);
this.initializer = await loadPluginBundle<TSetup, TStart, TPluginsSetup, TPluginsStart>(
addBasePath,
this.name
);
}
/**
@ -90,6 +104,21 @@ export class PluginWrapper<
return await this.instance.setup(setupContext, plugins);
}
/**
* Calls `setup` function exposed by the initialized plugin.
* @param startContext Context that consists of various core services tailored specifically
* for the `start` lifecycle event.
* @param plugins The dictionary where the key is the dependency name and the value
* is the contract returned by the dependency's `start` function.
*/
public async start(startContext: PluginStartContext, plugins: TPluginsStart) {
if (this.instance === undefined) {
throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`);
}
return await this.instance.start(startContext, plugins);
}
/**
* Calls optional `stop` function exposed by the plugin initializer.
*/
@ -114,6 +143,8 @@ export class PluginWrapper<
if (typeof instance.setup !== 'function') {
throw new Error(`Instance of plugin "${this.name}" does not define "setup" function.`);
} else if (typeof instance.start !== 'function') {
throw new Error(`Instance of plugin "${this.name}" does not define "start" function.`);
}
return instance;

View file

@ -22,11 +22,13 @@ import { BasePathSetup } from '../base_path';
import { ChromeSetup } from '../chrome';
import { CoreContext } from '../core_system';
import { FatalErrorsSetup } from '../fatal_errors';
import { I18nSetup } from '../i18n';
import { NotificationsSetup } from '../notifications';
import { I18nSetup, I18nStart } from '../i18n';
import { NotificationsSetup, NotificationsStart } from '../notifications';
import { UiSettingsSetup } from '../ui_settings';
import { PluginWrapper } from './plugin';
import { PluginsServiceSetupDeps } from './plugins_service';
import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service';
import { CapabilitiesStart } from '../capabilities';
import { OverlayStart } from '../overlays';
/**
* The available core services passed to a `PluginInitializer`
@ -50,6 +52,18 @@ export interface PluginSetupContext {
uiSettings: UiSettingsSetup;
}
/**
* The available core services passed to a plugin's `Plugin#start` method.
*
* @public
*/
export interface PluginStartContext {
capabilities: CapabilitiesStart;
i18n: I18nStart;
notifications: NotificationsStart;
overlays: OverlayStart;
}
/**
* Provides a plugin-specific context passed to the plugin's construtor. This is currently
* empty but should provide static services in the future, such as config and logging.
@ -75,10 +89,10 @@ export function createPluginInitializerContext(
* @param plugin
* @internal
*/
export function createPluginSetupContext<TPlugin, TPluginDependencies>(
export function createPluginSetupContext<TSetup, TStart, TPluginsSetup, TPluginsStart>(
coreContext: CoreContext,
deps: PluginsServiceSetupDeps,
plugin: PluginWrapper<TPlugin, TPluginDependencies>
plugin: PluginWrapper<TSetup, TStart, TPluginsSetup, TPluginsStart>
): PluginSetupContext {
return {
basePath: deps.basePath,
@ -89,3 +103,26 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
uiSettings: deps.uiSettings,
};
}
/**
* Provides a plugin-specific context passed to the plugin's `start` lifecycle event. Currently
* this returns a shallow copy the service start contracts, but in the future could provide
* plugin-scoped versions of the service.
*
* @param coreContext
* @param deps
* @param plugin
* @internal
*/
export function createPluginStartContext<TSetup, TStart, TPluginsSetup, TPluginsStart>(
coreContext: CoreContext,
deps: PluginsServiceStartDeps,
plugin: PluginWrapper<TSetup, TStart, TPluginsSetup, TPluginsStart>
): PluginStartContext {
return {
capabilities: deps.capabilities,
i18n: deps.i18n,
notifications: deps.notifications,
overlays: deps.overlays,
};
}

View file

@ -61,65 +61,74 @@ export const LOAD_TIMEOUT = 120 * 1000; // 2 minutes
*/
export const loadPluginBundle: LoadPluginBundle = <
TSetup,
TDependencies extends Record<string, unknown>
TStart,
TPluginsSetup extends Record<string, unknown>,
TPluginsStart extends Record<string, unknown>
>(
addBasePath: (path: string) => string,
pluginName: PluginName,
{ timeoutMs = LOAD_TIMEOUT } = {}
{ timeoutMs = LOAD_TIMEOUT }: { timeoutMs?: number } = {}
) =>
new Promise<PluginInitializer<TSetup, TDependencies>>((resolve, reject) => {
const script = document.createElement('script');
const coreWindow = (window as unknown) as CoreWindow;
new Promise<PluginInitializer<TSetup, TStart, TPluginsSetup, TPluginsStart>>(
(resolve, reject) => {
const script = document.createElement('script');
const coreWindow = (window as unknown) as CoreWindow;
// Assumes that all plugin bundles get put into the bundles/plugins subdirectory
const bundlePath = addBasePath(`/bundles/plugin/${pluginName}.bundle.js`);
script.setAttribute('src', bundlePath);
script.setAttribute('id', `kbn-plugin-${pluginName}`);
script.setAttribute('async', '');
// Assumes that all plugin bundles get put into the bundles/plugins subdirectory
const bundlePath = addBasePath(`/bundles/plugin/${pluginName}.bundle.js`);
script.setAttribute('src', bundlePath);
script.setAttribute('id', `kbn-plugin-${pluginName}`);
script.setAttribute('async', '');
// Add kbnNonce for CSP
script.setAttribute('nonce', coreWindow.__kbnNonce__);
// Add kbnNonce for CSP
script.setAttribute('nonce', coreWindow.__kbnNonce__);
const cleanupTag = () => {
clearTimeout(timeout);
// Set to null for IE memory leak issue. Webpack does the same thing.
// @ts-ignore
script.onload = script.onerror = null;
};
const cleanupTag = () => {
clearTimeout(timeout);
// Set to null for IE memory leak issue. Webpack does the same thing.
// @ts-ignore
script.onload = script.onerror = null;
};
// Wire up resolve and reject
script.onload = () => {
cleanupTag();
// Wire up resolve and reject
script.onload = () => {
cleanupTag();
const initializer = coreWindow.__kbnBundles__[`plugin/${pluginName}`];
if (!initializer || typeof initializer !== 'function') {
reject(
new Error(`Definition of plugin "${pluginName}" should be a function (${bundlePath}).`)
);
} else {
resolve(initializer as PluginInitializer<TSetup, TDependencies>);
}
};
const initializer = coreWindow.__kbnBundles__[`plugin/${pluginName}`];
if (!initializer || typeof initializer !== 'function') {
reject(
new Error(`Definition of plugin "${pluginName}" should be a function (${bundlePath}).`)
);
} else {
resolve(initializer as PluginInitializer<TSetup, TStart, TPluginsSetup, TPluginsStart>);
}
};
script.onerror = () => {
cleanupTag();
reject(new Error(`Failed to load "${pluginName}" bundle (${bundlePath})`));
};
script.onerror = () => {
cleanupTag();
reject(new Error(`Failed to load "${pluginName}" bundle (${bundlePath})`));
};
const timeout = setTimeout(() => {
cleanupTag();
reject(new Error(`Timeout reached when loading "${pluginName}" bundle (${bundlePath})`));
}, timeoutMs);
const timeout = setTimeout(() => {
cleanupTag();
reject(new Error(`Timeout reached when loading "${pluginName}" bundle (${bundlePath})`));
}, timeoutMs);
// Add the script tag to the end of the body to start downloading
document.body.appendChild(script);
});
// Add the script tag to the end of the body to start downloading
document.body.appendChild(script);
}
);
/**
* @internal
*/
export type LoadPluginBundle = <TSetup, TDependencies extends Record<string, unknown>>(
export type LoadPluginBundle = <
TSetup,
TStart,
TPluginsSetup extends Record<string, unknown>,
TPluginsStart extends Record<string, unknown>
>(
addBasePath: (path: string) => string,
pluginName: PluginName,
options?: { timeoutMs?: number }
) => Promise<PluginInitializer<TSetup, TDependencies>>;
) => Promise<PluginInitializer<TSetup, TStart, TPluginsSetup, TPluginsStart>>;

View file

@ -20,25 +20,36 @@
import { PluginsService, PluginsServiceSetup } from './plugins_service';
const createSetupContractMock = () => {
const setupContract: jest.Mocked<PublicMethodsOf<PluginsServiceSetup>> = {
pluginSetups: new Map(),
const setupContract: jest.Mocked<PluginsServiceSetup> = {
contracts: new Map(),
};
// we have to suppress type errors until decide how to mock es6 class
return (setupContract as unknown) as PluginsServiceSetup;
return setupContract as PluginsServiceSetup;
};
const createStartContractMock = () => {
const startContract: jest.Mocked<PluginsServiceSetup> = {
contracts: new Map(),
};
// we have to suppress type errors until decide how to mock es6 class
return startContract as PluginsServiceSetup;
};
type PluginsServiceContract = PublicMethodsOf<PluginsService>;
const createMock = () => {
const mocked: jest.Mocked<PluginsServiceContract> = {
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
};
mocked.setup.mockResolvedValue(createSetupContractMock());
mocked.start.mockResolvedValue(createStartContractMock());
return mocked;
};
export const pluginsServiceMock = {
create: createMock,
createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
};

View file

@ -27,7 +27,21 @@ import {
import { PluginName } from 'src/core/server';
import { CoreContext } from '../core_system';
import { PluginsService } from './plugins_service';
import {
PluginsService,
PluginsServiceStartDeps,
PluginsServiceSetupDeps,
} from './plugins_service';
import { notificationServiceMock } from '../notifications/notifications_service.mock';
import { capabilitiesServiceMock } from '../capabilities/capabilities_service.mock';
import { i18nServiceMock } from '../i18n/i18n_service.mock';
import { overlayServiceMock } from '../overlays/overlay_service.mock';
import { PluginStartContext, PluginSetupContext } from './plugin_context';
import { chromeServiceMock } from '../chrome/chrome_service.mock';
import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock';
import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock';
import { basePathServiceMock } from '../base_path/base_path_service.mock';
import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock';
export let mockPluginInitializers: Map<PluginName, MockedPluginInitializer>;
@ -35,41 +49,56 @@ mockPluginInitializerProvider.mockImplementation(
pluginName => mockPluginInitializers.get(pluginName)!
);
type DeeplyMocked<T> = { [P in keyof T]: jest.Mocked<T[P]> };
const mockCoreContext: CoreContext = {};
let mockDeps: any;
let mockInitContext: any;
let mockSetupDeps: DeeplyMocked<PluginsServiceSetupDeps>;
let mockSetupContext: DeeplyMocked<PluginSetupContext>;
let mockStartDeps: DeeplyMocked<PluginsServiceStartDeps>;
let mockStartContext: DeeplyMocked<PluginStartContext>;
beforeEach(() => {
mockDeps = {
injectedMetadata: {
getPlugins: jest.fn(() => [
mockSetupDeps = {
injectedMetadata: (function() {
const metadata = injectedMetadataServiceMock.createSetupContract();
metadata.getPlugins.mockReturnValue([
{ id: 'pluginA', plugin: createManifest('pluginA') },
{ id: 'pluginB', plugin: createManifest('pluginB', { required: ['pluginA'] }) },
{
id: 'pluginC',
plugin: createManifest('pluginC', { required: ['pluginA'], optional: ['nonexist'] }),
},
]),
},
basePath: {
addToPath(path: string) {
return path;
},
},
chrome: {},
fatalErrors: {},
i18n: {},
notifications: {},
uiSettings: {},
]);
return metadata;
})(),
basePath: (function() {
const basePath = basePathServiceMock.createSetupContract();
basePath.addToPath.mockImplementation(path => path);
return basePath;
})(),
chrome: chromeServiceMock.createSetupContract(),
fatalErrors: fatalErrorsServiceMock.createSetupContract(),
i18n: i18nServiceMock.createSetupContract(),
notifications: notificationServiceMock.createSetupContract(),
uiSettings: uiSettingsServiceMock.createSetupContract(),
} as any;
mockInitContext = omit(mockDeps, 'injectedMetadata');
mockSetupContext = omit(mockSetupDeps, 'injectedMetadata');
mockStartDeps = {
capabilities: capabilitiesServiceMock.createStartContract(),
i18n: i18nServiceMock.createStartContract(),
injectedMetadata: injectedMetadataServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
overlays: overlayServiceMock.createStartContract(),
};
mockStartContext = omit(mockStartDeps, 'injectedMetadata');
// Reset these for each test.
mockPluginInitializers = new Map<PluginName, MockedPluginInitializer>(([
[
'pluginA',
jest.fn(() => ({
setup: jest.fn(() => ({ exportedValue: 1 })),
setup: jest.fn(() => ({ setupValue: 1 })),
start: jest.fn(() => ({ startValue: 2 })),
stop: jest.fn(),
})),
],
@ -77,7 +106,10 @@ beforeEach(() => {
'pluginB',
jest.fn(() => ({
setup: jest.fn((core, deps: any) => ({
pluginAPlusB: deps.pluginA.exportedValue + 1,
pluginAPlusB: deps.pluginA.setupValue + 1,
})),
start: jest.fn((core, deps: any) => ({
pluginAPlusB: deps.pluginA.startValue + 1,
})),
stop: jest.fn(),
})),
@ -86,6 +118,7 @@ beforeEach(() => {
'pluginC',
jest.fn(() => ({
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
})),
],
@ -113,7 +146,7 @@ test('`PluginsService.setup` fails if any bundle cannot be loaded', async () =>
mockLoadPluginBundle.mockRejectedValueOnce(new Error('Could not load bundle'));
const pluginsService = new PluginsService(mockCoreContext);
await expect(pluginsService.setup(mockDeps)).rejects.toThrowErrorMatchingInlineSnapshot(
await expect(pluginsService.setup(mockSetupDeps)).rejects.toThrowErrorMatchingInlineSnapshot(
`"Could not load bundle"`
);
});
@ -121,24 +154,24 @@ test('`PluginsService.setup` fails if any bundle cannot be loaded', async () =>
test('`PluginsService.setup` fails if any plugin instance does not have a setup function', async () => {
mockPluginInitializers.set('pluginA', (() => ({})) as any);
const pluginsService = new PluginsService(mockCoreContext);
await expect(pluginsService.setup(mockDeps)).rejects.toThrowErrorMatchingInlineSnapshot(
await expect(pluginsService.setup(mockSetupDeps)).rejects.toThrowErrorMatchingInlineSnapshot(
`"Instance of plugin \\"pluginA\\" does not define \\"setup\\" function."`
);
});
test('`PluginsService.setup` calls loadPluginBundles with basePath and plugins', async () => {
const pluginsService = new PluginsService(mockCoreContext);
await pluginsService.setup(mockDeps);
await pluginsService.setup(mockSetupDeps);
expect(mockLoadPluginBundle).toHaveBeenCalledTimes(3);
expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockDeps.basePath.addToPath, 'pluginA');
expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockDeps.basePath.addToPath, 'pluginB');
expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockDeps.basePath.addToPath, 'pluginC');
expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockSetupDeps.basePath.addToPath, 'pluginA');
expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockSetupDeps.basePath.addToPath, 'pluginB');
expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockSetupDeps.basePath.addToPath, 'pluginC');
});
test('`PluginsService.setup` initalizes plugins with CoreContext', async () => {
const pluginsService = new PluginsService(mockCoreContext);
await pluginsService.setup(mockDeps);
await pluginsService.setup(mockSetupDeps);
expect(mockPluginInitializers.get('pluginA')).toHaveBeenCalledWith(mockCoreContext);
expect(mockPluginInitializers.get('pluginB')).toHaveBeenCalledWith(mockCoreContext);
@ -147,50 +180,102 @@ test('`PluginsService.setup` initalizes plugins with CoreContext', async () => {
test('`PluginsService.setup` exposes dependent setup contracts to plugins', async () => {
const pluginsService = new PluginsService(mockCoreContext);
await pluginsService.setup(mockDeps);
await pluginsService.setup(mockSetupDeps);
const pluginAInstance = mockPluginInitializers.get('pluginA')!.mock.results[0].value;
const pluginBInstance = mockPluginInitializers.get('pluginB')!.mock.results[0].value;
const pluginCInstance = mockPluginInitializers.get('pluginC')!.mock.results[0].value;
expect(pluginAInstance.setup).toHaveBeenCalledWith(mockInitContext, {});
expect(pluginBInstance.setup).toHaveBeenCalledWith(mockInitContext, {
pluginA: { exportedValue: 1 },
expect(pluginAInstance.setup).toHaveBeenCalledWith(mockSetupContext, {});
expect(pluginBInstance.setup).toHaveBeenCalledWith(mockSetupContext, {
pluginA: { setupValue: 1 },
});
// Does not supply value for `nonexist` optional dep
expect(pluginCInstance.setup).toHaveBeenCalledWith(mockInitContext, {
pluginA: { exportedValue: 1 },
expect(pluginCInstance.setup).toHaveBeenCalledWith(mockSetupContext, {
pluginA: { setupValue: 1 },
});
});
test('`PluginsService.setup` does not set missing dependent setup contracts', async () => {
mockDeps.injectedMetadata.getPlugins.mockReturnValue([
mockSetupDeps.injectedMetadata.getPlugins.mockReturnValue([
{ id: 'pluginD', plugin: createManifest('pluginD', { required: ['missing'] }) },
]);
mockPluginInitializers.set('pluginD', jest.fn(() => ({ setup: jest.fn() })) as any);
mockPluginInitializers.set('pluginD', jest.fn(() => ({
setup: jest.fn(),
start: jest.fn(),
})) as any);
const pluginsService = new PluginsService(mockCoreContext);
await pluginsService.setup(mockDeps);
await pluginsService.setup(mockSetupDeps);
// If a dependency is missing it should not be in the deps at all, not even as undefined.
const pluginDInstance = mockPluginInitializers.get('pluginD')!.mock.results[0].value;
expect(pluginDInstance.setup).toHaveBeenCalledWith(mockInitContext, {});
expect(pluginDInstance.setup).toHaveBeenCalledWith(mockSetupContext, {});
const pluginDDeps = pluginDInstance.setup.mock.calls[0][1];
expect(pluginDDeps).not.toHaveProperty('missing');
});
test('`PluginsService.setup` returns plugin setup contracts', async () => {
const pluginsService = new PluginsService(mockCoreContext);
const { contracts } = await pluginsService.setup(mockDeps);
const { contracts } = await pluginsService.setup(mockSetupDeps);
// Verify that plugin contracts were available
expect((contracts.get('pluginA')! as any).exportedValue).toEqual(1);
expect((contracts.get('pluginA')! as any).setupValue).toEqual(1);
expect((contracts.get('pluginB')! as any).pluginAPlusB).toEqual(2);
});
test('`PluginsService.start` exposes dependent start contracts to plugins', async () => {
const pluginsService = new PluginsService(mockCoreContext);
await pluginsService.setup(mockSetupDeps);
await pluginsService.start(mockStartDeps);
const pluginAInstance = mockPluginInitializers.get('pluginA')!.mock.results[0].value;
const pluginBInstance = mockPluginInitializers.get('pluginB')!.mock.results[0].value;
const pluginCInstance = mockPluginInitializers.get('pluginC')!.mock.results[0].value;
expect(pluginAInstance.start).toHaveBeenCalledWith(mockStartContext, {});
expect(pluginBInstance.start).toHaveBeenCalledWith(mockStartContext, {
pluginA: { startValue: 2 },
});
// Does not supply value for `nonexist` optional dep
expect(pluginCInstance.start).toHaveBeenCalledWith(mockStartContext, {
pluginA: { startValue: 2 },
});
});
test('`PluginsService.start` does not set missing dependent start contracts', async () => {
mockSetupDeps.injectedMetadata.getPlugins.mockReturnValue([
{ id: 'pluginD', plugin: createManifest('pluginD', { required: ['missing'] }) },
]);
mockPluginInitializers.set('pluginD', jest.fn(() => ({
setup: jest.fn(),
start: jest.fn(),
})) as any);
const pluginsService = new PluginsService(mockCoreContext);
await pluginsService.setup(mockSetupDeps);
await pluginsService.start(mockStartDeps);
// If a dependency is missing it should not be in the deps at all, not even as undefined.
const pluginDInstance = mockPluginInitializers.get('pluginD')!.mock.results[0].value;
expect(pluginDInstance.start).toHaveBeenCalledWith(mockStartContext, {});
const pluginDDeps = pluginDInstance.start.mock.calls[0][1];
expect(pluginDDeps).not.toHaveProperty('missing');
});
test('`PluginsService.start` returns plugin start contracts', async () => {
const pluginsService = new PluginsService(mockCoreContext);
await pluginsService.setup(mockSetupDeps);
const { contracts } = await pluginsService.start(mockStartDeps);
// Verify that plugin contracts were available
expect((contracts.get('pluginA')! as any).startValue).toEqual(2);
expect((contracts.get('pluginB')! as any).pluginAPlusB).toEqual(3);
});
test('`PluginService.stop` calls the stop function on each plugin', async () => {
const pluginsService = new PluginsService(mockCoreContext);
await pluginsService.setup(mockDeps);
await pluginsService.setup(mockSetupDeps);
const pluginAInstance = mockPluginInitializers.get('pluginA')!.mock.results[0].value;
const pluginBInstance = mockPluginInitializers.get('pluginB')!.mock.results[0].value;

View file

@ -17,15 +17,21 @@
* under the License.
*/
import { CoreSetup } from '..';
import { CoreSetup, CoreStart } from '..';
import { PluginName } from '../../server';
import { CoreService } from '../../types';
import { CoreContext } from '../core_system';
import { PluginWrapper } from './plugin';
import { createPluginInitializerContext, createPluginSetupContext } from './plugin_context';
import {
createPluginInitializerContext,
createPluginSetupContext,
createPluginStartContext,
} from './plugin_context';
/** @internal */
export type PluginsServiceSetupDeps = CoreSetup;
/** @internal */
export type PluginsServiceStartDeps = CoreStart;
/** @internal */
export interface PluginsServiceSetup {
@ -98,6 +104,41 @@ export class PluginsService implements CoreService<PluginsServiceSetup> {
return { contracts };
}
public async start(deps: PluginsServiceStartDeps) {
// Setup each plugin with required and optional plugin contracts
const contracts = new Map<string, unknown>();
for (const [pluginName, plugin] of this.plugins.entries()) {
const pluginDeps = new Set([
...plugin.requiredPlugins,
...plugin.optionalPlugins.filter(optPlugin => this.plugins.get(optPlugin)),
]);
const pluginDepContracts = [...pluginDeps.keys()].reduce(
(depContracts, dependencyName) => {
// Only set if present. Could be absent if plugin does not have client-side code or is a
// missing optional plugin.
if (contracts.has(dependencyName)) {
depContracts[dependencyName] = contracts.get(dependencyName);
}
return depContracts;
},
{} as Record<PluginName, unknown>
);
contracts.set(
pluginName,
await plugin.start(
createPluginStartContext(this.coreContext, deps, plugin),
pluginDepContracts
)
);
}
// Expose start contracts
return { contracts };
}
public async stop() {
// Stop plugins in reverse topological order.
for (const pluginName of this.satupPlugins.reverse()) {

View file

@ -6,7 +6,6 @@
import * as CSS from 'csstype';
import { default } from 'react';
import { Observable } from 'rxjs';
import * as PropTypes from 'prop-types';
import * as Rx from 'rxjs';
import { Toast } from '@elastic/eui';
@ -29,7 +28,7 @@ export interface Capabilities {
}
// @public
export interface CapabilitiesSetup {
export interface CapabilitiesStart {
getCapabilities: () => Capabilities;
}
@ -66,8 +65,6 @@ export interface CoreSetup {
// (undocumented)
basePath: BasePathSetup;
// (undocumented)
capabilities: CapabilitiesSetup;
// (undocumented)
chrome: ChromeSetup;
// (undocumented)
fatalErrors: FatalErrorsSetup;
@ -80,11 +77,23 @@ export interface CoreSetup {
// (undocumented)
notifications: NotificationsSetup;
// (undocumented)
overlays: OverlaySetup;
// (undocumented)
uiSettings: UiSettingsSetup;
}
// @public (undocumented)
export interface CoreStart {
// (undocumented)
capabilities: CapabilitiesStart;
// (undocumented)
i18n: I18nStart;
// (undocumented)
injectedMetadata: InjectedMetadataStart;
// (undocumented)
notifications: NotificationsStart;
// (undocumented)
overlays: OverlayStart;
}
// @internal
export class CoreSystem {
constructor(params: Params);
@ -93,6 +102,8 @@ export class CoreSystem {
fatalErrors: import(".").FatalErrorsSetup;
} | undefined>;
// (undocumented)
start(): Promise<void>;
// (undocumented)
stop(): void;
}
@ -119,6 +130,9 @@ export interface I18nSetup {
}) => JSX.Element;
}
// @public (undocumented)
export type I18nStart = I18nSetup;
// @internal (undocumented)
export interface InjectedMetadataParams {
// (undocumented)
@ -197,10 +211,19 @@ export interface InjectedMetadataSetup {
}
// @public (undocumented)
export type NotificationsSetup = ReturnType<NotificationsService['setup']>;
export type InjectedMetadataStart = InjectedMetadataSetup;
// @public (undocumented)
export interface OverlaySetup {
export interface NotificationsSetup {
// (undocumented)
toasts: ToastsApi;
}
// @public (undocumented)
export type NotificationsStart = NotificationsSetup;
// @public (undocumented)
export interface OverlayStart {
// (undocumented)
openFlyout: (flyoutChildren: React.ReactNode, flyoutProps?: {
closeButtonAriaLabel?: string;
@ -209,15 +232,17 @@ export interface OverlaySetup {
}
// @public
export interface Plugin<TSetup, TPluginsSetup extends Record<string, unknown> = {}> {
export interface Plugin<TSetup, TStart, TPluginsSetup extends Record<string, unknown> = {}, TPluginsStart extends Record<string, unknown> = {}> {
// (undocumented)
setup: (core: PluginSetupContext, plugins: TPluginsSetup) => TSetup | Promise<TSetup>;
// (undocumented)
start: (core: PluginStartContext, plugins: TPluginsStart) => TStart | Promise<TStart>;
// (undocumented)
stop?: () => void;
}
// @public
export type PluginInitializer<TSetup, TPluginsSetup extends Record<string, unknown> = {}> = (core: PluginInitializerContext) => Plugin<TSetup, TPluginsSetup>;
export type PluginInitializer<TSetup, TStart, TPluginsSetup extends Record<string, unknown> = {}, TPluginsStart extends Record<string, unknown> = {}> = (core: PluginInitializerContext) => Plugin<TSetup, TStart, TPluginsSetup, TPluginsStart>;
// @public
export interface PluginInitializerContext {
@ -245,7 +270,7 @@ export { Toast }
export type ToastInput = string | Pick<Toast, Exclude<keyof Toast, 'id'>>;
// @public (undocumented)
export class ToastsSetup {
export class ToastsApi {
// (undocumented)
add(toastOrTitle: ToastInput): Toast;
// (undocumented)

View file

@ -18,11 +18,11 @@
*/
import { uiModules } from 'ui/modules';
import { uiCapabilities } from 'ui/capabilities';
import { capabilities } from 'ui/capabilities';
uiModules.get('kibana')
.provider('dashboardConfig', () => {
let hideWriteControls = !uiCapabilities.dashboard.showWriteControls;
let hideWriteControls = !capabilities.get().dashboard.showWriteControls;
return {
/**

View file

@ -21,7 +21,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { uiCapabilities } from 'ui/capabilities';
import { capabilities } from 'ui/capabilities';
import { toastNotifications } from 'ui/notify';
import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder';
@ -88,7 +88,7 @@ export class DashboardAddPanel extends React.Component {
)}
/>
</EuiFlyoutBody>
{ uiCapabilities.visualize.save ? (
{ capabilities.get().visualize.save ? (
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>

View file

@ -27,11 +27,13 @@ import {
jest.mock('ui/capabilities',
() => ({
uiCapabilities: {
visualize: {
show: true,
save: true
}
capabilities: {
get: () => ({
visualize: {
show: true,
save: true
}
})
}
}), { virtual: true });

View file

@ -24,7 +24,7 @@ import 'ui/directives/css_truncate';
import 'ui/directives/field_name';
import './string_progress_bar';
import detailsHtml from './lib/detail_views/string.html';
import { uiCapabilities } from 'ui/capabilities';
import { capabilities } from 'ui/capabilities';
import { uiModules } from 'ui/modules';
const app = uiModules.get('apps/discover');
@ -77,7 +77,7 @@ app.directive('discoverField', function ($compile, i18n) {
};
$scope.canVisualize = uiCapabilities.visualize.show;
$scope.canVisualize = capabilities.get().visualize.show;
$scope.toggleDisplay = function (field) {
if (field.display) {

View file

@ -18,7 +18,7 @@
*/
import '../doc_table';
import { uiCapabilities } from 'ui/capabilities';
import { capabilities } from 'ui/capabilities';
import { i18n } from '@kbn/i18n';
import { EmbeddableFactory } from 'ui/embeddable';
import {
@ -63,7 +63,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory {
onEmbeddableStateChanged: OnEmbeddableStateChanged
) {
const editUrl = this.getEditPath(id);
const editable = uiCapabilities.discover.save as boolean;
const editable = capabilities.get().discover.save as boolean;
// can't change this to be async / awayt, because an Anglular promise is expected to be returned.
return this.searchLoader.get(id).then(savedObject => {

View file

@ -20,7 +20,7 @@
import { management } from 'ui/management';
import uiRoutes from 'ui/routes';
import { uiModules } from 'ui/modules';
import { uiCapabilities } from 'ui/capabilities';
import { capabilities } from 'ui/capabilities';
import { I18nContext } from 'ui/i18n';
import indexTemplate from './index.html';
import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue';
@ -45,7 +45,7 @@ function updateAdvancedSettings($scope, config, query) {
<AdvancedSettings
config={config}
query={query}
enableSaving={uiCapabilities.advancedSettings.save}
enableSaving={capabilities.get().advancedSettings.save}
/>
</I18nContext>,
node,

View file

@ -24,7 +24,7 @@ import 'ui/vis/editors/default/sidebar';
import 'ui/visualize';
import 'ui/collapsible_sidebar';
import 'ui/query_bar';
import { uiCapabilities } from 'ui/capabilities';
import { capabilities } from 'ui/capabilities';
import 'ui/search_bar';
import 'ui/apply_filters';
import 'ui/listen';
@ -150,7 +150,7 @@ function VisEditor(
dirty: !savedVis.id
};
$scope.topNavMenu = [...(uiCapabilities.visualize.save ? [{
$scope.topNavMenu = [...(capabilities.get().visualize.save ? [{
key: i18n('kbn.topNavMenu.saveVisualizationButtonLabel', { defaultMessage: 'save' }),
description: i18n('kbn.visualize.topNavMenu.saveVisualizationButtonAriaLabel', {
defaultMessage: 'Save Visualization',
@ -215,7 +215,7 @@ function VisEditor(
showShareContextMenu({
anchorElement,
allowEmbed: true,
allowShortUrl: uiCapabilities.visualize.createShortUrl,
allowShortUrl: capabilities.get().visualize.createShortUrl,
getUnhashableStates,
objectId: savedVis.id,
objectType: 'visualization',

View file

@ -23,7 +23,7 @@ import { EmbeddableFactory } from 'ui/embeddable';
import { getVisualizeLoader } from 'ui/visualize/loader';
import { Legacy } from 'kibana';
import { uiCapabilities } from 'ui/capabilities';
import { capabilities } from 'ui/capabilities';
import {
EmbeddableInstanceConfiguration,
OnEmbeddableStateChanged,
@ -90,7 +90,7 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory<VisualizationA
) {
const visId = panelMetadata.id;
const editUrl = this.getEditPath(visId);
const editable: boolean = uiCapabilities.visualize.save as boolean;
const editable: boolean = capabilities.get().visualize.save as boolean;
const loader = await getVisualizeLoader();
const savedObject = await this.savedVisualizations.get(visId);

View file

@ -21,7 +21,7 @@ import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { uiCapabilities } from 'ui/capabilities';
import { capabilities } from 'ui/capabilities';
import { TableListView } from './../../table_list_view';
import {
@ -46,8 +46,8 @@ class VisualizeListingTableUi extends Component {
// for data exploration purposes
createItem={this.props.createItem}
findItems={this.props.findItems}
deleteItems={uiCapabilities.visualize.delete ? this.props.deleteItems : null}
editItem={uiCapabilities.visualize.save ? this.props.editItem : null}
deleteItems={capabilities.get().visualize.delete ? this.props.deleteItems : null}
editItem={capabilities.get().visualize.save ? this.props.editItem : null}
tableColumns={this.getTableColumns()}
listingLimit={100}
initialFilter={''}

View file

@ -43,7 +43,7 @@ import { CoreSystem } from '__kibanaCore__'
const rootDomElement = document.createElement('div');
document.body.appendChild(rootDomElement)
new CoreSystem({
const coreSystem = new CoreSystem({
injectedMetadata: {
version: '1.2.3',
buildNumber: 1234,
@ -113,5 +113,11 @@ new CoreSystem({
requireLegacyFiles: () => {
${bundle.getRequires().join('\n ')}
}
}).setup()
})
coreSystem
.setup()
.then(() => {
return coreSystem.start();
});
`;

View file

@ -19,7 +19,7 @@
import _ from 'lodash';
import { uiCapabilities } from 'ui/capabilities';
import { capabilities } from 'ui/capabilities';
import { DocTitleProvider } from 'ui/doc_title';
import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry';
import { notify, fatalError, toastNotifications } from 'ui/notify';
@ -269,7 +269,7 @@ app.controller('timelion', function (
testId: 'timelionDocsButton',
};
if (uiCapabilities.timelion.save) {
if (capabilities.get().timelion.save) {
return [newSheetAction, addSheetAction, saveSheetAction, deleteSheetAction, openSheetAction, optionsAction, helpAction];
}
return [newSheetAction, addSheetAction, openSheetAction, optionsAction, helpAction];

View file

@ -17,15 +17,28 @@
* under the License.
*/
import { Capabilities as UICapabilities, CapabilitiesSetup } from '../../../../core/public';
import { Capabilities as UICapabilities, CapabilitiesStart } from '../../../../core/public';
export { Capabilities as UICapabilities } from '../../../../core/public';
export let uiCapabilities: UICapabilities = null!;
export { UICapabilities };
export function __newPlatformInit__(capabililitiesService: CapabilitiesSetup) {
let uiCapabilities: UICapabilities = null!;
export function __newPlatformStart__(capabililitiesService: CapabilitiesStart) {
if (uiCapabilities) {
throw new Error('ui/capabilities already initialized with new platform apis');
}
uiCapabilities = capabililitiesService.getCapabilities();
}
export const capabilities = {
get() {
if (!uiCapabilities) {
throw new Error(
`UI Capabilities are only available in the legacy platform once Angular has booted.`
);
}
return uiCapabilities;
},
};

View file

@ -18,11 +18,13 @@
*/
jest.mock('ui/capabilities', () => ({
uiCapabilities: {
uiCapability1: true,
uiCapability2: {
nestedProp: 'nestedValue',
},
capabilities: {
get: () => ({
uiCapability1: true,
uiCapability2: {
nestedProp: 'nestedValue',
},
}),
},
}));

View file

@ -18,11 +18,13 @@
*/
jest.mock('ui/capabilities', () => ({
uiCapabilities: {
uiCapability1: true,
uiCapability2: {
nestedProp: 'nestedValue',
},
capabilities: {
get: () => ({
uiCapability1: true,
uiCapability2: {
nestedProp: 'nestedValue',
},
}),
},
}));

View file

@ -19,7 +19,7 @@
import PropTypes from 'prop-types';
import React, { ReactNode } from 'react';
import { uiCapabilities, UICapabilities } from '../..';
import { capabilities, UICapabilities } from '../..';
interface Props {
children: ReactNode;
@ -38,7 +38,7 @@ export class UICapabilitiesProvider extends React.Component<Props, {}> {
public getChildContext(): ProviderContext {
return {
uiCapabilities,
uiCapabilities: capabilities.get(),
};
}

View file

@ -18,7 +18,7 @@
*/
import React, { ReactNode } from 'react';
import { uiCapabilities } from '..';
import { capabilities } from '..';
import { UICapabilitiesContext } from './ui_capabilities_context';
interface Props {
@ -30,7 +30,7 @@ export class UICapabilitiesProvider extends React.Component<Props, {}> {
public render() {
return (
<UICapabilitiesContext.Provider value={uiCapabilities}>
<UICapabilitiesContext.Provider value={capabilities.get()}>
{this.props.children}
</UICapabilitiesContext.Provider>
);

View file

@ -18,7 +18,7 @@
*/
import { basePathServiceMock } from '../../../../../core/public/mocks';
import { __newPlatformInit__, initChromeBasePathApi } from './base_path';
import { __newPlatformSetup__, initChromeBasePathApi } from './base_path';
function initChrome() {
const chrome: any = {};
@ -27,7 +27,7 @@ function initChrome() {
}
const newPlatformBasePath = basePathServiceMock.createSetupContract();
__newPlatformInit__(newPlatformBasePath);
__newPlatformSetup__(newPlatformBasePath);
beforeEach(() => {
jest.clearAllMocks();

View file

@ -20,7 +20,7 @@
import { BasePathSetup } from '../../../../../core/public';
let newPlatformBasePath: BasePathSetup;
export function __newPlatformInit__(instance: BasePathSetup) {
export function __newPlatformSetup__(instance: BasePathSetup) {
if (newPlatformBasePath) {
throw new Error('ui/chrome/api/base_path is already initialized');
}

View file

@ -23,7 +23,7 @@ export type Breadcrumb = ChromeBreadcrumb;
export type BreadcrumbsApi = ReturnType<typeof createBreadcrumbsApi>['breadcrumbs'];
let newPlatformChrome: ChromeSetup;
export function __newPlatformInit__(instance: ChromeSetup) {
export function __newPlatformSetup__(instance: ChromeSetup) {
if (newPlatformChrome) {
throw new Error('ui/chrome/api/breadcrumbs is already initialized');
}

View file

@ -20,11 +20,11 @@
import * as Rx from 'rxjs';
import { chromeServiceMock } from '../../../../../core/public/mocks';
import { __newPlatformInit__, initChromeControlsApi } from './controls';
import { __newPlatformSetup__, initChromeControlsApi } from './controls';
const newPlatformChrome = chromeServiceMock.createSetupContract();
__newPlatformInit__(newPlatformChrome);
__newPlatformSetup__(newPlatformChrome);
function setup() {
const isVisible$ = new Rx.BehaviorSubject(true);

View file

@ -22,7 +22,7 @@ import { ChromeSetup } from '../../../../../core/public';
let newPlatformChrome: ChromeSetup;
export function __newPlatformInit__(instance: ChromeSetup) {
export function __newPlatformSetup__(instance: ChromeSetup) {
if (newPlatformChrome) {
throw new Error('ui/chrome/api/controls is already initialized');
}

View file

@ -20,7 +20,7 @@
import { ChromeHelpExtension, ChromeSetup } from '../../../../../core/public';
let newPlatformChrome: ChromeSetup;
export function __newPlatformInit__(instance: ChromeSetup) {
export function __newPlatformSetup__(instance: ChromeSetup) {
if (newPlatformChrome) {
throw new Error('ui/chrome/api/help_extension is already initialized');
}

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { __newPlatformInit__, initChromeInjectedVarsApi } from './injected_vars';
import { __newPlatformSetup__, initChromeInjectedVarsApi } from './injected_vars';
function initChrome() {
const chrome: any = {};
@ -29,7 +29,7 @@ const newPlatformInjectedMetadata: any = {
getInjectedVars: jest.fn(),
getInjectedVar: jest.fn(),
};
__newPlatformInit__(newPlatformInjectedMetadata);
__newPlatformSetup__(newPlatformInjectedMetadata);
beforeEach(() => {
jest.resetAllMocks();

View file

@ -22,7 +22,7 @@ import { InjectedMetadataSetup } from '../../../../../core/public';
let newPlatformInjectedVars: InjectedMetadataSetup;
export function __newPlatformInit__(instance: InjectedMetadataSetup) {
export function __newPlatformSetup__(instance: InjectedMetadataSetup) {
if (newPlatformInjectedVars) {
throw new Error('ui/chrome/api/injected_vars is already initialized');
}

View file

@ -21,7 +21,7 @@ import * as Rx from 'rxjs';
let newPlatformHttp;
export function __newPlatformInit__(instance) {
export function __newPlatformSetup__(instance) {
if (newPlatformHttp) {
throw new Error('ui/chrome/api/loading_count already initialized with new platform apis');
}

View file

@ -20,11 +20,11 @@
import * as Rx from 'rxjs';
import { chromeServiceMock } from '../../../../../core/public/mocks';
import { __newPlatformInit__, initChromeThemeApi } from './theme';
import { __newPlatformSetup__, initChromeThemeApi } from './theme';
const newPlatformChrome = chromeServiceMock.createSetupContract();
__newPlatformInit__(newPlatformChrome);
__newPlatformSetup__(newPlatformChrome);
function setup() {
const brand$ = new Rx.BehaviorSubject({ logo: 'foo', smallLogo: 'foo' });

View file

@ -23,7 +23,7 @@ import { ChromeBrand, ChromeSetup } from '../../../../../core/public';
let newPlatformChrome: ChromeSetup;
export function __newPlatformInit__(instance: ChromeSetup) {
export function __newPlatformSetup__(instance: ChromeSetup) {
if (newPlatformChrome) {
throw new Error('ui/chrome/api/theme is already initialized');
}

View file

@ -19,7 +19,7 @@
let newPlatformUiSettingsClient;
export function __newPlatformInit__(instance) {
export function __newPlatformSetup__(instance) {
if (newPlatformUiSettingsClient) {
throw new Error('ui/chrome/api/ui_settings already initialized');
}

View file

@ -21,7 +21,7 @@ import { distinctUntilChanged } from 'rxjs/operators';
import { uiModules } from '../../modules';
let newPlatformChrome;
export function __newPlatformInit__(instance) {
export function __newPlatformSetup__(instance) {
if (newPlatformChrome) {
throw new Error('ui/chrome/global_nav_state is already initialized');
}

View file

@ -21,11 +21,11 @@ import { render } from 'enzyme';
import PropTypes from 'prop-types';
import React from 'react';
import { __newPlatformInit__, wrapInI18nContext } from '.';
import { __newPlatformSetup__, wrapInI18nContext } from '.';
describe('ui/i18n', () => {
test('renders children and forwards properties', () => {
__newPlatformInit__(({ children }) => <div>Context: {children}</div>);
__newPlatformSetup__(({ children }) => <div>Context: {children}</div>);
const mockPropTypes = {
stringProp: PropTypes.string.isRequired,

View file

@ -25,7 +25,7 @@ import { uiModules } from 'ui/modules';
import { I18nSetup } from '../../../../core/public';
export let I18nContext: I18nSetup['Context'] = null!;
export function __newPlatformInit__(context: typeof I18nContext) {
export function __newPlatformSetup__(context: typeof I18nContext) {
if (I18nContext) {
throw new Error('ui/i18n already initialized with new platform apis');
}

View file

@ -73,7 +73,7 @@ function open(adapters: Adapters, options: InspectorOptions = {}): InspectorSess
if an inspector can be shown.`);
}
return getNewPlatform().setup.core.overlays.openFlyout(
return getNewPlatform().start.core.overlays.openFlyout(
<InspectorPanel views={views} adapters={adapters} title={options.title} />,
{
'data-test-subj': 'inspectorPanel',

View file

@ -19,7 +19,7 @@
import { assign } from 'lodash';
import { IndexedArray } from '../indexed_array';
import { uiCapabilities } from '../capabilities';
import { capabilities } from '../capabilities';
const listeners = [];
@ -56,7 +56,7 @@ export class ManagementSection {
get visibleItems() {
return this.items.inOrder.filter(item => {
const capabilityManagementSection = uiCapabilities.management[this.id];
const capabilityManagementSection = capabilities.get().management[this.id];
const itemCapability = capabilityManagementSection ? capabilityManagementSection[item.id] : null;
return item.visible && itemCapability !== false;

View file

@ -17,14 +17,16 @@
* under the License.
*/
jest.mock('ui/capabilities', () => ({
uiCapabilities: {
navLinks: {},
management: {
kibana: {
sampleFeature1: true,
sampleFeature2: false,
capabilities: {
get: () => ({
navLinks: {},
management: {
kibana: {
sampleFeature1: true,
sampleFeature2: false,
}
}
}
})
}
}));

View file

@ -19,7 +19,7 @@
export let metadata = null;
export function __newPlatformInit__(legacyMetadata) {
export function __newPlatformSetup__(legacyMetadata) {
if (metadata === null) {
metadata = legacyMetadata;
} else {

View file

@ -16,4 +16,4 @@
* specific language governing permissions and limitations
* under the License.
*/
export { __newPlatformInit__, getNewPlatform } from './new_platform';
export { __newPlatformSetup__, __newPlatformStart__, getNewPlatform } from './new_platform';

View file

@ -16,26 +16,39 @@
* specific language governing permissions and limitations
* under the License.
*/
import { CoreSetup } from '../../../../core/public';
import { CoreSetup, CoreStart } from '../../../../core/public';
const runtimeContext = {
setup: {
core: (null as unknown) as CoreSetup,
plugins: {},
},
start: {
core: (null as unknown) as CoreStart,
plugins: {},
},
};
export function __newPlatformInit__(core: CoreSetup) {
export function __newPlatformSetup__(core: CoreSetup) {
if (runtimeContext.setup.core) {
throw new Error('New platform core api was already initialized');
throw new Error('New platform core api was already set up');
}
runtimeContext.setup.core = core;
}
export function __newPlatformStart__(core: CoreStart) {
if (runtimeContext.start.core) {
throw new Error('New platform core api was already started');
}
runtimeContext.start.core = core;
}
export function getNewPlatform() {
if (runtimeContext.setup.core === null) {
if (runtimeContext.setup.core === null || runtimeContext.start.core === null) {
throw new Error('runtimeContext is not initialized yet');
}
return runtimeContext;
}

View file

@ -26,7 +26,7 @@ import {
let newPlatformFatalErrors: FatalErrorsSetup;
export function __newPlatformInit__(instance: FatalErrorsSetup) {
export function __newPlatformSetup__(instance: FatalErrorsSetup) {
if (newPlatformFatalErrors) {
throw new Error('ui/notify/fatal_error already initialized with new platform apis');
}

View file

@ -17,5 +17,5 @@
* under the License.
*/
export { toastNotifications, __newPlatformInit__ } from './toasts';
export { toastNotifications, __newPlatformSetup__ } from './toasts';
export { Toast, ToastInput } from './toast_notifications';

View file

@ -18,14 +18,14 @@
*/
import sinon from 'sinon';
import { ToastsSetup } from '../../../../../core/public';
import { ToastsApi } from '../../../../../core/public';
import { ToastNotifications } from './toast_notifications';
describe('ToastNotifications', () => {
describe('interface', () => {
function setup() {
return { toastNotifications: new ToastNotifications(new ToastsSetup()) };
return { toastNotifications: new ToastNotifications(new ToastsApi()) };
}
describe('add method', () => {

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { Toast, ToastInput, ToastsSetup } from '../../../../../core/public';
import { Toast, ToastInput, ToastsApi } from '../../../../../core/public';
export { Toast, ToastInput };
@ -26,7 +26,7 @@ export class ToastNotifications {
private onChangeCallback?: () => void;
constructor(private readonly toasts: ToastsSetup) {
constructor(private readonly toasts: ToastsApi) {
toasts.get$().subscribe(list => {
this.list = list;

View file

@ -17,12 +17,12 @@
* under the License.
*/
import { ToastsSetup } from '../../../../../core/public';
import { ToastsApi } from '../../../../../core/public';
import { ToastNotifications } from './toast_notifications';
export let toastNotifications: ToastNotifications;
export function __newPlatformInit__(toasts: ToastsSetup) {
export function __newPlatformSetup__(toasts: ToastsApi) {
if (toastNotifications) {
throw new Error('ui/notify/toasts already initialized with new platform apis');
}

View file

@ -18,7 +18,7 @@
*/
import { uiRegistry } from './_registry';
import { uiCapabilities } from '../capabilities';
import { capabilities } from '../capabilities';
export const FeatureCatalogueRegistryProvider = uiRegistry({
name: 'featureCatalogue',
@ -26,7 +26,7 @@ export const FeatureCatalogueRegistryProvider = uiRegistry({
group: ['category'],
order: ['title'],
filter: featureCatalogItem => {
const isDisabledViaCapabilities = uiCapabilities.catalogue[featureCatalogItem.id] === false;
const isDisabledViaCapabilities = capabilities.get().catalogue[featureCatalogItem.id] === false;
return !isDisabledViaCapabilities && Object.keys(featureCatalogItem).length > 0;
}
});

View file

@ -17,14 +17,16 @@
* under the License.
*/
jest.mock('ui/capabilities', () => ({
uiCapabilities: {
navLinks: {},
management: {},
catalogue: {
item1: true,
item2: false,
item3: true,
},
capabilities: {
get: () => ({
navLinks: {},
management: {},
catalogue: {
item1: true,
item2: false,
item3: true,
},
}),
}
}));
import { FeatureCatalogueCategory, FeatureCatalogueRegistryProvider } from './feature_catalogue';

View file

@ -62,6 +62,8 @@ i18n.load(injectedMetadata.i18n.translationsUrl)
if (i18nError) {
coreSetup.fatalErrors.add(i18nError);
}
return coreSystem.start();
});
});
`;

View file

@ -18,6 +18,7 @@
*/
import { PluginInitializer } from 'kibana/public';
import { TestbedPlugin, TestbedPluginSetup } from './plugin';
import { TestbedPlugin, TestbedPluginSetup, TestbedPluginStart } from './plugin';
export const plugin: PluginInitializer<TestbedPluginSetup> = () => new TestbedPlugin();
export const plugin: PluginInitializer<TestbedPluginSetup, TestbedPluginStart> = () =>
new TestbedPlugin();

View file

@ -19,11 +19,17 @@
import { Plugin, PluginSetupContext } from 'kibana/public';
export class TestbedPlugin implements Plugin<TestbedPluginSetup> {
export class TestbedPlugin implements Plugin<TestbedPluginSetup, TestbedPluginStart> {
public setup(core: PluginSetupContext, deps: {}) {
// eslint-disable-next-line no-console
console.log(`Testbed plugin loaded`);
console.log(`Testbed plugin set up`);
}
public start() {
// eslint-disable-next-line no-console
console.log(`Testbed plugin started`);
}
}
export type TestbedPluginSetup = ReturnType<TestbedPlugin['setup']>;
export type TestbedPluginStart = ReturnType<TestbedPlugin['start']>;

View file

@ -18,6 +18,7 @@
*/
import { PluginInitializer } from 'kibana/public';
import { CorePluginAPlugin, CorePluginAPluginSetup } from './plugin';
import { CorePluginAPlugin, CorePluginAPluginSetup, CorePluginAPluginStart } from './plugin';
export const plugin: PluginInitializer<CorePluginAPluginSetup> = () => new CorePluginAPlugin();
export const plugin: PluginInitializer<CorePluginAPluginSetup, CorePluginAPluginStart> = () =>
new CorePluginAPlugin();

View file

@ -19,7 +19,7 @@
import { Plugin, PluginSetupContext } from 'kibana/public';
export class CorePluginAPlugin implements Plugin<CorePluginAPluginSetup> {
export class CorePluginAPlugin implements Plugin<CorePluginAPluginSetup, CorePluginAPluginStart> {
public setup(core: PluginSetupContext, deps: {}) {
return {
getGreeting() {
@ -27,6 +27,9 @@ export class CorePluginAPlugin implements Plugin<CorePluginAPluginSetup> {
},
};
}
public start() {}
}
export type CorePluginAPluginSetup = ReturnType<CorePluginAPlugin['setup']>;
export type CorePluginAPluginStart = ReturnType<CorePluginAPlugin['start']>;

View file

@ -18,7 +18,15 @@
*/
import { PluginInitializer } from 'kibana/public';
import { CorePluginBDeps, CorePluginBPlugin, CorePluginBPluginSetup } from './plugin';
import {
CorePluginBDeps,
CorePluginBPlugin,
CorePluginBPluginSetup,
CorePluginBPluginStart,
} from './plugin';
export const plugin: PluginInitializer<CorePluginBPluginSetup, CorePluginBDeps> = () =>
new CorePluginBPlugin();
export const plugin: PluginInitializer<
CorePluginBPluginSetup,
CorePluginBPluginStart,
CorePluginBDeps
> = () => new CorePluginBPlugin();

View file

@ -30,10 +30,14 @@ export interface CorePluginBDeps {
core_plugin_a: CorePluginAPluginSetup;
}
export class CorePluginBPlugin implements Plugin<CorePluginBPluginSetup, CorePluginBDeps> {
export class CorePluginBPlugin
implements Plugin<CorePluginBPluginSetup, CorePluginBPluginStart, CorePluginBDeps> {
public setup(core: PluginSetupContext, deps: CorePluginBDeps) {
window.corePluginB = `Plugin A said: ${deps.core_plugin_a.getGreeting()}`;
}
public start() {}
}
export type CorePluginBPluginSetup = ReturnType<CorePluginBPlugin['setup']>;
export type CorePluginBPluginStart = ReturnType<CorePluginBPlugin['start']>;

View file

@ -38,7 +38,7 @@ class SamplePanelAction extends ContextMenuAction {
if (!embeddable) {
return;
}
getNewPlatform().setup.core.overlays.openFlyout(
getNewPlatform().start.core.overlays.openFlyout(
<React.Fragment>
<EuiFlyoutHeader>
<EuiTitle size="s" data-test-subj="samplePanelActionTitle">

View file

@ -15,14 +15,17 @@ let internals: UICapabilities = {
},
};
export const uiCapabilities = new Proxy(
{},
{
get: (target, property) => {
return internals[String(property)] as any;
},
}
);
export const capabilities = {
get: () =>
new Proxy(
{},
{
get: (target, property) => {
return internals[String(property)] as any;
},
}
),
};
export function setMockCapabilities(mockCapabilities: UICapabilities) {
internals = mockCapabilities;

View file

@ -5,7 +5,7 @@
*/
import { get } from 'lodash';
import { uiCapabilities } from 'ui/capabilities';
import { capabilities } from 'ui/capabilities';
import { getDefaultWorkpad } from './defaults';
export const getInitialState = path => {
@ -13,7 +13,7 @@ export const getInitialState = path => {
app: {}, // Kibana stuff in here
assets: {}, // assets end up here
transient: {
canUserWrite: uiCapabilities.canvas.save,
canUserWrite: capabilities.get().canvas.save,
elementStats: {
total: 0,
ready: 0,

View file

@ -47,7 +47,7 @@ import {
import {
getOutlinkEncoders,
} from './services/outlink_encoders';
import { uiCapabilities } from 'ui/capabilities';
import { capabilities } from 'ui/capabilities';
const app = uiModules.get('app/graph');
@ -804,7 +804,7 @@ app.controller('graphuiPlugin', function ($scope, $route, $http, kbnUrl, Private
// if saving is disabled using uiCapabilities, we don't want to render the save
// button so it's consistent with all of the other applications
if (uiCapabilities.graph.save) {
if (capabilities.get().graph.save) {
// allSavingDisabled is based on the xpack.graph.savePolicy, we'll maintain this functionality
if (!$scope.allSavingDisabled) {
$scope.topNavMenu.push({
@ -855,7 +855,7 @@ app.controller('graphuiPlugin', function ($scope, $route, $http, kbnUrl, Private
});
// if deleting is disabled using uiCapabilities, we don't want to render the delete
// button so it's consistent with all of the other applications
if (uiCapabilities.graph.delete) {
if (capabilities.get().graph.delete) {
// allSavingDisabled is based on the xpack.graph.savePolicy, we'll maintain this functionality
if (!$scope.allSavingDisabled) {

View file

@ -10,7 +10,7 @@ import 'ui/listen';
import React from 'react';
import { I18nProvider } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { uiCapabilities } from 'ui/capabilities';
import { capabilities } from 'ui/capabilities';
import { render, unmountComponentAtNode } from 'react-dom';
import { uiModules } from 'ui/modules';
import { timefilter } from 'ui/timefilter';
@ -127,7 +127,7 @@ app.controller('GisMapController', ($scope, $route, config, kbnUrl, localStorage
// clear old UI state
store.dispatch(setSelectedLayer(null));
store.dispatch(updateFlyout(FLYOUT_STATE.NONE));
store.dispatch(setReadOnly(!uiCapabilities.maps.save));
store.dispatch(setReadOnly(!capabilities.get().maps.save));
handleStoreChanges(store);
unsubscribe = store.subscribe(() => {
@ -297,7 +297,7 @@ app.controller('GisMapController', ($scope, $route, config, kbnUrl, localStorage
const inspectorAdapters = getInspectorAdapters(store.getState());
Inspector.open(inspectorAdapters, {});
}
}, ...(uiCapabilities.maps.save ? [{
}, ...(capabilities.get().maps.save ? [{
key: i18n.translate('xpack.maps.mapController.saveMapButtonLabel', {
defaultMessage: `save`
}),

View file

@ -14,7 +14,7 @@ import 'uiExports/search';
import 'uiExports/embeddableFactories';
import 'ui/agg_types';
import { uiCapabilities } from 'ui/capabilities';
import { capabilities } from 'ui/capabilities';
import chrome from 'ui/chrome';
import routes from 'ui/routes';
import 'ui/kbn_top_nav';
@ -53,7 +53,7 @@ routes
$scope.delete = (ids) => {
return gisMapSavedObjectLoader.delete(ids);
};
$scope.readOnly = !uiCapabilities.maps.save;
$scope.readOnly = !capabilities.get().maps.save;
},
resolve: {
hasMaps: function (kbnUrl) {

View file

@ -6,7 +6,7 @@
import _ from 'lodash';
import routes from 'ui/routes';
import { uiCapabilities } from 'ui/capabilities';
import { capabilities } from 'ui/capabilities';
import { kfetch } from 'ui/kfetch';
import { fatalError } from 'ui/notify';
import template from 'plugins/security/views/management/edit_role/edit_role.html';
@ -146,7 +146,7 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, {
allowFieldLevelSecurity={allowFieldLevelSecurity}
spaces={spaces}
spacesEnabled={enableSpaceAwarePrivileges}
uiCapabilities={uiCapabilities}
uiCapabilities={capabilities.get()}
features={features}
privileges={privileges}
/>

Some files were not shown because too many files have changed in this diff Show more