Spaces - migrate default space and enter space view to KP (#66098)

This commit is contained in:
Larry Gregory 2020-05-12 19:59:29 -04:00 committed by GitHub
parent 531eb7cadc
commit dc8dd19543
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 601 additions and 70 deletions

View file

@ -13,9 +13,6 @@ import { SpacesPluginSetup } from '../../../plugins/spaces/server';
// @ts-ignore
import { AuditLogger } from '../../server/lib/audit_logger';
import { wrapError } from './server/lib/errors';
// @ts-ignore
import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize';
import { initEnterSpaceView } from './server/routes/views';
export interface LegacySpacesPlugin {
getSpaceId: (request: Legacy.Request) => ReturnType<SpacesServiceSetup['getSpaceId']>;
@ -50,7 +47,7 @@ export const spaces = (kibana: Record<string, any>) =>
) {
// NOTICE: use of `activeSpace` is deprecated and will not be made available in the New Platform.
// Known usages:
// - x-pack/legacy/plugins/infra/public/utils/use_kibana_space_id.ts
// - x-pack/plugins/infra/public/utils/use_kibana_space_id.ts
const spacesPlugin = server.newPlatform.setup.plugins.spaces as SpacesPluginSetup;
if (!spacesPlugin) {
throw new Error('New Platform XPack Spaces plugin is not available.');
@ -82,7 +79,7 @@ export const spaces = (kibana: Record<string, any>) =>
throw new Error('New Platform XPack Spaces plugin is not available.');
}
const { registerLegacyAPI, createDefaultSpace } = spacesPlugin.__legacyCompat;
const { registerLegacyAPI } = spacesPlugin.__legacyCompat;
registerLegacyAPI({
auditLogger: {
@ -91,12 +88,6 @@ export const spaces = (kibana: Record<string, any>) =>
},
});
initEnterSpaceView(server);
watchStatusAndLicenseToInitialize(server.plugins.xpack_main, this, async () => {
await createDefaultSpace();
});
server.expose('getSpaceId', (request: Legacy.Request) =>
spacesPlugin.spacesService.getSpaceId(request)
);

View file

@ -1,30 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Legacy } from 'kibana';
import { ENTER_SPACE_PATH } from '../../../../../../plugins/spaces/common/constants';
import { wrapError } from '../../lib/errors';
export function initEnterSpaceView(server: Legacy.Server) {
server.route({
method: 'GET',
path: ENTER_SPACE_PATH,
async handler(request, h) {
try {
const uiSettings = request.getUiSettingsService();
const defaultRoute = await uiSettings.get<string>('defaultRoute');
const basePath = server.newPlatform.setup.core.http.basePath.get(request);
const url = `${basePath}${defaultRoute}`;
return h.redirect(url);
} catch (e) {
server.log(['spaces', 'error'], `Error navigating to space: ${e}`);
return wrapError(e);
}
},
});
}

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { SpacesLicense } from '.';
export const licenseMock = {
create: (): jest.Mocked<SpacesLicense> => ({
isEnabled: jest.fn().mockReturnValue(true),
}),
};

View file

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

View file

@ -0,0 +1,46 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { of } from 'rxjs';
import { licensingMock } from '../../../licensing/public/mocks';
import { SpacesLicenseService } from './license_service';
import { LICENSE_TYPE, LicenseType } from '../../../licensing/common/types';
describe('license#isEnabled', function() {
it('should indicate that Spaces is disabled when there is no license information', () => {
const serviceSetup = new SpacesLicenseService().setup({
license$: of(undefined as any),
});
expect(serviceSetup.license.isEnabled()).toEqual(false);
});
it('should indicate that Spaces is disabled when xpack is unavailable', () => {
const rawLicenseMock = licensingMock.createLicenseMock();
rawLicenseMock.isAvailable = false;
const serviceSetup = new SpacesLicenseService().setup({
license$: of(rawLicenseMock),
});
expect(serviceSetup.license.isEnabled()).toEqual(false);
});
for (const level in LICENSE_TYPE) {
if (isNaN(level as any)) {
it(`should indicate that Spaces is enabled with a ${level} license`, () => {
const rawLicense = licensingMock.createLicense({
license: {
status: 'active',
type: level as LicenseType,
},
});
const serviceSetup = new SpacesLicenseService().setup({
license$: of(rawLicense),
});
expect(serviceSetup.license.isEnabled()).toEqual(true);
});
}
}
});

View file

@ -0,0 +1,50 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Observable, Subscription } from 'rxjs';
import { ILicense } from '../../../licensing/common/types';
export interface SpacesLicense {
isEnabled(): boolean;
}
interface SetupDeps {
license$: Observable<ILicense>;
}
export class SpacesLicenseService {
private licenseSubscription?: Subscription;
public setup({ license$ }: SetupDeps) {
let rawLicense: Readonly<ILicense> | undefined;
this.licenseSubscription = license$.subscribe(nextRawLicense => {
rawLicense = nextRawLicense;
});
return {
license: Object.freeze({
isEnabled: () => this.isSpacesEnabledFromRawLicense(rawLicense),
}),
};
}
public stop() {
if (this.licenseSubscription) {
this.licenseSubscription.unsubscribe();
this.licenseSubscription = undefined;
}
}
private isSpacesEnabledFromRawLicense(rawLicense: Readonly<ILicense> | undefined) {
if (!rawLicense || !rawLicense.isAvailable) {
return false;
}
const licenseCheck = rawLicense.check('spaces', 'basic');
return licenseCheck.state !== 'unavailable' && licenseCheck.state !== 'invalid';
}
}

View file

@ -6,6 +6,7 @@
import { createDefaultSpace } from './create_default_space';
import { SavedObjectsErrorHelpers } from 'src/core/server';
import { loggingServiceMock } from '../../../../../src/core/server/mocks';
interface MockServerSettings {
defaultExists?: boolean;
@ -47,14 +48,16 @@ const createMockDeps = (settings: MockServerSettings = {}) => {
});
return {
savedObjects: {
createInternalRepository: jest.fn().mockImplementation(() => {
return {
get: mockGet,
create: mockCreate,
};
getSavedObjects: () =>
Promise.resolve({
createInternalRepository: jest.fn().mockImplementation(() => {
return {
get: mockGet,
create: mockCreate,
};
}),
}),
},
logger: loggingServiceMock.createLogger(),
};
};
@ -65,7 +68,7 @@ test(`it creates the default space when one does not exist`, async () => {
await createDefaultSpace(deps);
const repository = deps.savedObjects.createInternalRepository();
const repository = (await deps.getSavedObjects()).createInternalRepository();
expect(repository.get).toHaveBeenCalledTimes(1);
expect(repository.create).toHaveBeenCalledTimes(1);
@ -89,7 +92,7 @@ test(`it does not attempt to recreate the default space if it already exists`, a
await createDefaultSpace(deps);
const repository = deps.savedObjects.createInternalRepository();
const repository = (await deps.getSavedObjects()).createInternalRepository();
expect(repository.get).toHaveBeenCalledTimes(1);
expect(repository.create).toHaveBeenCalledTimes(0);
@ -114,7 +117,7 @@ test(`it ignores conflict errors if the default space already exists`, async ()
await createDefaultSpace(deps);
const repository = deps.savedObjects.createInternalRepository();
const repository = (await deps.getSavedObjects()).createInternalRepository();
expect(repository.get).toHaveBeenCalledTimes(1);
expect(repository.create).toHaveBeenCalledTimes(1);

View file

@ -5,22 +5,26 @@
*/
import { i18n } from '@kbn/i18n';
import { SavedObjectsServiceStart, SavedObjectsRepository } from 'src/core/server';
import { SavedObjectsServiceStart, SavedObjectsRepository, Logger } from 'src/core/server';
import { SavedObjectsErrorHelpers } from '../../../../../src/core/server';
import { DEFAULT_SPACE_ID } from '../../common/constants';
interface Deps {
savedObjects: Pick<SavedObjectsServiceStart, 'createInternalRepository'>;
getSavedObjects: () => Promise<Pick<SavedObjectsServiceStart, 'createInternalRepository'>>;
logger: Logger;
}
export async function createDefaultSpace({ savedObjects }: Deps) {
const { createInternalRepository } = savedObjects;
export async function createDefaultSpace({ getSavedObjects, logger }: Deps) {
const { createInternalRepository } = await getSavedObjects();
const savedObjectsRepository = createInternalRepository(['space']);
logger.debug('Checking for existing default space');
const defaultSpaceExists = await doesDefaultSpaceExist(savedObjectsRepository);
if (defaultSpaceExists) {
logger.debug('Default space already exists');
return;
}
@ -28,6 +32,7 @@ export async function createDefaultSpace({ savedObjects }: Deps) {
id: DEFAULT_SPACE_ID,
};
logger.debug('Creating the default space');
try {
await savedObjectsRepository.create(
'space',
@ -53,6 +58,8 @@ export async function createDefaultSpace({ savedObjects }: Deps) {
}
throw error;
}
logger.debug('Default space created');
}
async function doesDefaultSpaceExist(savedObjectsRepository: Pick<SavedObjectsRepository, 'get'>) {

View file

@ -0,0 +1,280 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as Rx from 'rxjs';
import {
DefaultSpaceService,
RETRY_SCALE_DURATION,
RETRY_DURATION_MAX,
} from './default_space_service';
import {
ServiceStatusLevels,
ServiceStatusLevel,
CoreStatus,
SavedObjectsRepository,
SavedObjectsErrorHelpers,
} from '../../../../../src/core/server';
import { coreMock, loggingServiceMock } from 'src/core/server/mocks';
import { licensingMock } from '../../../licensing/server/mocks';
import { SpacesLicenseService } from '../../common/licensing';
import { ILicense } from '../../../licensing/server';
import { nextTick } from 'test_utils/enzyme_helpers';
import { first } from 'rxjs/operators';
const advanceRetry = async (initializeCount: number) => {
await Promise.resolve();
let duration = initializeCount * RETRY_SCALE_DURATION;
if (duration > RETRY_DURATION_MAX) {
duration = RETRY_DURATION_MAX;
}
jest.advanceTimersByTime(duration);
};
interface SetupOpts {
elasticsearchStatus: ServiceStatusLevel;
savedObjectsStatus: ServiceStatusLevel;
license: ILicense;
}
const setup = ({ elasticsearchStatus, savedObjectsStatus, license }: SetupOpts) => {
const core = coreMock.createSetup();
const { status } = core;
status.core$ = (new Rx.BehaviorSubject({
elasticsearch: {
level: elasticsearchStatus,
summary: '',
},
savedObjects: {
level: savedObjectsStatus,
summary: '',
},
}) as unknown) as Rx.Observable<CoreStatus>;
const { savedObjects } = coreMock.createStart();
const repository = savedObjects.createInternalRepository() as jest.Mocked<SavedObjectsRepository>;
// simulate space not found
repository.get.mockRejectedValue(SavedObjectsErrorHelpers.createGenericNotFoundError());
repository.create.mockReturnValue(Promise.resolve({} as any));
const license$ = new Rx.BehaviorSubject(license);
const logger = loggingServiceMock.createLogger();
const { license: spacesLicense } = new SpacesLicenseService().setup({ license$ });
const defaultSpaceService = new DefaultSpaceService();
const { serviceStatus$ } = defaultSpaceService.setup({
coreStatus: status,
getSavedObjects: () => Promise.resolve(savedObjects),
license$,
logger,
spacesLicense,
});
return {
coreStatus: (status as unknown) as { core$: Rx.BehaviorSubject<CoreStatus> },
serviceStatus$,
logger,
license$,
savedObjects,
repository,
};
};
test(`does not initialize if elasticsearch is unavailable`, async () => {
const { repository, serviceStatus$ } = setup({
elasticsearchStatus: ServiceStatusLevels.unavailable,
savedObjectsStatus: ServiceStatusLevels.available,
license: licensingMock.createLicense({
license: {
status: 'active',
type: 'gold',
},
}),
});
await nextTick();
expect(repository.get).not.toHaveBeenCalled();
expect(repository.create).not.toHaveBeenCalled();
const status = await serviceStatus$.pipe(first()).toPromise();
expect(status.level).toEqual(ServiceStatusLevels.unavailable);
expect(status.summary).toMatchInlineSnapshot(`"required core services are not ready"`);
});
test(`does not initialize if savedObjects is unavailable`, async () => {
const { serviceStatus$, repository } = setup({
elasticsearchStatus: ServiceStatusLevels.available,
savedObjectsStatus: ServiceStatusLevels.unavailable,
license: licensingMock.createLicense({
license: {
status: 'active',
type: 'gold',
},
}),
});
await nextTick();
expect(repository.get).not.toHaveBeenCalled();
expect(repository.create).not.toHaveBeenCalled();
const status = await serviceStatus$.pipe(first()).toPromise();
expect(status.level).toEqual(ServiceStatusLevels.unavailable);
expect(status.summary).toMatchInlineSnapshot(`"required core services are not ready"`);
});
test(`does not initialize if the license is unavailable`, async () => {
const license = licensingMock.createLicense({
license: ({ type: ' ', status: ' ' } as unknown) as ILicense,
}) as Writable<ILicense>;
license.isAvailable = false;
const { serviceStatus$, repository } = setup({
elasticsearchStatus: ServiceStatusLevels.available,
savedObjectsStatus: ServiceStatusLevels.available,
license,
});
await nextTick();
expect(repository.get).not.toHaveBeenCalled();
expect(repository.create).not.toHaveBeenCalled();
const status = await serviceStatus$.pipe(first()).toPromise();
expect(status.level).toEqual(ServiceStatusLevels.unavailable);
expect(status.summary).toMatchInlineSnapshot(`"missing or invalid license"`);
});
test(`initializes once all dependencies are met`, async () => {
const { repository, coreStatus, serviceStatus$ } = setup({
elasticsearchStatus: ServiceStatusLevels.available,
savedObjectsStatus: ServiceStatusLevels.unavailable,
license: licensingMock.createLicense({
license: {
type: 'gold',
status: 'active',
},
}),
});
await nextTick();
expect(repository.get).not.toHaveBeenCalled();
expect(repository.create).not.toHaveBeenCalled();
const status = await serviceStatus$.pipe(first()).toPromise();
expect(status.level).toEqual(ServiceStatusLevels.unavailable);
expect(status.summary).toMatchInlineSnapshot(`"required core services are not ready"`);
coreStatus.core$.next({
elasticsearch: {
level: ServiceStatusLevels.available,
summary: '',
},
savedObjects: {
level: ServiceStatusLevels.available,
summary: '',
},
});
await nextTick();
expect(repository.get).toHaveBeenCalled();
expect(repository.create).toHaveBeenCalled();
const nextStatus = await serviceStatus$.pipe(first()).toPromise();
expect(nextStatus.level).toEqual(ServiceStatusLevels.available);
expect(nextStatus.summary).toMatchInlineSnapshot(`"ready"`);
});
test('maintains unavailable status if default space cannot be created', async () => {
const { repository, serviceStatus$ } = setup({
elasticsearchStatus: ServiceStatusLevels.available,
savedObjectsStatus: ServiceStatusLevels.available,
license: licensingMock.createLicense({
license: {
type: 'gold',
status: 'active',
},
}),
});
repository.create.mockRejectedValue(new Error('something bad happened'));
await nextTick();
expect(repository.get).toHaveBeenCalled();
expect(repository.create).toHaveBeenCalled();
const status = await serviceStatus$.pipe(first()).toPromise();
expect(status.level).toEqual(ServiceStatusLevels.unavailable);
expect(status.summary).toMatchInlineSnapshot(
`"Error creating default space: something bad happened"`
);
});
test('retries operation', async () => {
jest.useFakeTimers();
const { repository, serviceStatus$ } = setup({
elasticsearchStatus: ServiceStatusLevels.available,
savedObjectsStatus: ServiceStatusLevels.available,
license: licensingMock.createLicense({
license: {
type: 'gold',
status: 'active',
},
}),
});
repository.create.mockRejectedValue(new Error('something bad happened'));
await nextTick();
expect(repository.get).toHaveBeenCalledTimes(1);
expect(repository.create).toHaveBeenCalledTimes(1);
let status = await serviceStatus$.pipe(first()).toPromise();
expect(status.level).toEqual(ServiceStatusLevels.unavailable);
expect(status.summary).toMatchInlineSnapshot(
`"Error creating default space: something bad happened"`
);
await advanceRetry(1);
await nextTick();
expect(repository.get).toHaveBeenCalledTimes(2);
expect(repository.create).toHaveBeenCalledTimes(2);
status = await serviceStatus$.pipe(first()).toPromise();
expect(status.level).toEqual(ServiceStatusLevels.unavailable);
expect(status.summary).toMatchInlineSnapshot(
`"Error creating default space: something bad happened"`
);
repository.create.mockResolvedValue({} as any);
// retries are scaled back, so this should not cause the repository to be invoked
await advanceRetry(1);
await nextTick();
expect(repository.get).toHaveBeenCalledTimes(2);
expect(repository.create).toHaveBeenCalledTimes(2);
status = await serviceStatus$.pipe(first()).toPromise();
expect(status.level).toEqual(ServiceStatusLevels.unavailable);
expect(status.summary).toMatchInlineSnapshot(
`"Error creating default space: something bad happened"`
);
await advanceRetry(1);
await nextTick();
expect(repository.get).toHaveBeenCalledTimes(3);
expect(repository.create).toHaveBeenCalledTimes(3);
status = await serviceStatus$.pipe(first()).toPromise();
expect(status.level).toEqual(ServiceStatusLevels.available);
expect(status.summary).toMatchInlineSnapshot(`"ready"`);
});

View file

@ -0,0 +1,128 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { CoreSetup, SavedObjectsServiceStart, Logger, ServiceStatus } from 'src/core/server';
import {
concat,
of,
timer,
Observable,
ObservableInput,
combineLatest,
defer,
Subscription,
BehaviorSubject,
} from 'rxjs';
import { mergeMap, switchMap, catchError, tap } from 'rxjs/operators';
import { ServiceStatusLevels } from '../../../../../src/core/server';
import { ILicense } from '../../../licensing/server';
import { SpacesLicense } from '../../common/licensing';
import { createDefaultSpace } from './create_default_space';
interface Deps {
coreStatus: CoreSetup['status'];
getSavedObjects: () => Promise<Pick<SavedObjectsServiceStart, 'createInternalRepository'>>;
license$: Observable<ILicense>;
spacesLicense: SpacesLicense;
logger: Logger;
}
export const RETRY_SCALE_DURATION = 100;
export const RETRY_DURATION_MAX = 10000;
const calculateDuration = (i: number) => {
const duration = i * RETRY_SCALE_DURATION;
if (duration > RETRY_DURATION_MAX) {
return RETRY_DURATION_MAX;
}
return duration;
};
// we can't use a retryWhen here, because we want to propagate the unavailable status and then retry
const propagateUnavailableStatusAndScaleRetry = () => {
let i = 0;
return (err: Error, caught: ObservableInput<any>) =>
concat(
of({
level: ServiceStatusLevels.unavailable,
summary: `Error creating default space: ${err.message}`,
}),
timer(calculateDuration(++i)).pipe(mergeMap(() => caught))
);
};
export class DefaultSpaceService {
private initializeSubscription?: Subscription;
private serviceStatus$?: BehaviorSubject<ServiceStatus>;
public setup({ coreStatus, getSavedObjects, license$, spacesLicense, logger }: Deps) {
const statusLogger = logger.get('status');
this.serviceStatus$ = new BehaviorSubject({
level: ServiceStatusLevels.unavailable,
summary: 'not initialized',
} as ServiceStatus);
this.initializeSubscription = combineLatest([coreStatus.core$, license$])
.pipe(
switchMap(([status]) => {
const isElasticsearchReady = status.elasticsearch.level === ServiceStatusLevels.available;
const isSavedObjectsReady = status.savedObjects.level === ServiceStatusLevels.available;
if (!isElasticsearchReady || !isSavedObjectsReady) {
return of({
level: ServiceStatusLevels.unavailable,
summary: 'required core services are not ready',
} as ServiceStatus);
}
if (!spacesLicense.isEnabled()) {
return of({
level: ServiceStatusLevels.unavailable,
summary: 'missing or invalid license',
} as ServiceStatus);
}
return defer(() =>
createDefaultSpace({
getSavedObjects,
logger,
}).then(() => {
return {
level: ServiceStatusLevels.available,
summary: 'ready',
};
})
).pipe(catchError(propagateUnavailableStatusAndScaleRetry()));
}),
tap<ServiceStatus>(spacesStatus => {
// This is temporary for debugging/visibility until we get a proper status service from core.
// See issue #41983 for details.
statusLogger.debug(`${spacesStatus.level.toString()}: ${spacesStatus.summary}`);
this.serviceStatus$!.next(spacesStatus);
})
)
.subscribe();
return {
serviceStatus$: this.serviceStatus$!.asObservable(),
};
}
public stop() {
if (this.initializeSubscription) {
this.initializeSubscription.unsubscribe();
}
this.initializeSubscription = undefined;
if (this.serviceStatus$) {
this.serviceStatus$.complete();
this.serviceStatus$ = undefined;
}
}
}

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { initEnterSpaceView } from './enter_space';
export { DefaultSpaceService } from './default_space_service';

View file

@ -24,7 +24,6 @@ describe('Spaces Plugin', () => {
expect(spacesSetup).toMatchInlineSnapshot(`
Object {
"__legacyCompat": Object {
"createDefaultSpace": [Function],
"registerLegacyAPI": [Function],
},
"spacesService": Object {

View file

@ -14,7 +14,6 @@ import {
} from '../../features/server';
import { SecurityPluginSetup } from '../../security/server';
import { LicensingPluginSetup } from '../../licensing/server';
import { createDefaultSpace } from './lib/create_default_space';
// @ts-ignore
import { AuditLogger } from '../../../../server/lib/audit_logger';
import { SpacesAuditLogger } from './lib/audit_logger';
@ -29,6 +28,8 @@ import { initInternalSpacesApi } from './routes/api/internal';
import { initSpacesViewsRoutes } from './routes/views';
import { setupCapabilities } from './capabilities';
import { SpacesSavedObjectsService } from './saved_objects';
import { DefaultSpaceService } from './default_space';
import { SpacesLicenseService } from '../common/licensing';
/**
* Describes a set of APIs that is available in the legacy platform only and required by this plugin
@ -56,10 +57,6 @@ export interface SpacesPluginSetup {
spacesService: SpacesServiceSetup;
__legacyCompat: {
registerLegacyAPI: (legacyAPI: LegacyAPI) => void;
// TODO: We currently need the legacy plugin to inform this plugin when it is safe to create the default space.
// The NP does not have the equivilent ES connection/health/comapt checks that the legacy world does.
// See: https://github.com/elastic/kibana/issues/43456
createDefaultSpace: () => Promise<void>;
};
}
@ -72,6 +69,10 @@ export class Plugin {
private readonly log: Logger;
private readonly spacesLicenseService = new SpacesLicenseService();
private defaultSpaceService?: DefaultSpaceService;
private legacyAPI?: LegacyAPI;
private readonly getLegacyAPI = () => {
if (!this.legacyAPI) {
@ -115,8 +116,21 @@ export class Plugin {
const savedObjectsService = new SpacesSavedObjectsService();
savedObjectsService.setup({ core, spacesService });
const { license } = this.spacesLicenseService.setup({ license$: plugins.licensing.license$ });
this.defaultSpaceService = new DefaultSpaceService();
this.defaultSpaceService.setup({
coreStatus: core.status,
getSavedObjects: async () => (await core.getStartServices())[0].savedObjects,
license$: plugins.licensing.license$,
spacesLicense: license,
logger: this.log,
});
initSpacesViewsRoutes({
httpResources: core.http.resources,
basePath: core.http.basePath,
logger: this.log,
});
const externalRouter = core.http.createRouter();
@ -167,15 +181,13 @@ export class Plugin {
registerLegacyAPI: (legacyAPI: LegacyAPI) => {
this.legacyAPI = legacyAPI;
},
createDefaultSpace: async () => {
const [coreStart] = await core.getStartServices();
return await createDefaultSpace({
savedObjects: coreStart.savedObjects,
});
},
},
};
}
public stop() {}
public stop() {
if (this.defaultSpaceService) {
this.defaultSpaceService.stop();
}
}
}

View file

@ -4,10 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { HttpResources } from 'src/core/server';
import { HttpResources, IBasePath, Logger } from 'src/core/server';
import { ENTER_SPACE_PATH } from '../../../common';
import { wrapError } from '../../lib/errors';
export interface ViewRouteDeps {
httpResources: HttpResources;
basePath: IBasePath;
logger: Logger;
}
export function initSpacesViewsRoutes(deps: ViewRouteDeps) {
@ -15,4 +19,25 @@ export function initSpacesViewsRoutes(deps: ViewRouteDeps) {
{ path: '/spaces/space_selector', validate: false },
(context, request, response) => response.renderCoreApp()
);
deps.httpResources.register(
{ path: ENTER_SPACE_PATH, validate: false },
async (context, request, response) => {
try {
const defaultRoute = await context.core.uiSettings.client.get<string>('defaultRoute');
const basePath = deps.basePath.get(request);
const url = `${basePath}${defaultRoute}`;
return response.redirected({
headers: {
location: url,
},
});
} catch (e) {
deps.logger.error(`Error navigating to space: ${e}`);
return response.customError(wrapError(e));
}
}
);
}