Skip to content

Commit

Permalink
ILM locators (#102313)
Browse files Browse the repository at this point in the history
* feat: ๐ŸŽธ add url service types

* refactor: ๐Ÿ’ก move locator types into its own folder

* feat: ๐ŸŽธ add abstract locator implementation

* feat: ๐ŸŽธ implement abstract locator client

* feat: ๐ŸŽธ add browser-side locators service

* feat: ๐ŸŽธ implement locator .getLocation()

* feat: ๐ŸŽธ implement navigate function

* feat: ๐ŸŽธ implement locator service in /common folder

* feat: ๐ŸŽธ expose locators client on browser and server

* refactor: ๐Ÿ’ก make locators async

* chore: ๐Ÿค– add deprecation notice to URL generators

* docs: โœ๏ธ add deprecation notice to readme

* feat: ๐ŸŽธ create management app locator

* refactor: ๐Ÿ’ก simplify management locator

* feat: ๐ŸŽธ export management app locator from plugin contract

* feat: ๐ŸŽธ implement ILM locator

* feat: ๐ŸŽธ improve share plugin exports

* feat: ๐ŸŽธ improve management app locator

* feat: ๐ŸŽธ add useLocatorUrl React hook

* feat: ๐ŸŽธ add .getUrl() method to locators

* feat: ๐ŸŽธ migrate ILM app to use URL locators

* fix: ๐Ÿ› correct typescript errors

* Fix TypeScript errors in mock

* Fix ILM locator unit tests

* style: ๐Ÿ’„ shorten import

Co-authored-by: Vadim Kibana <vadimkibana@gmail.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
3 people authored Jun 21, 2021
1 parent ccf6039 commit 1fb2640
Show file tree
Hide file tree
Showing 30 changed files with 311 additions and 175 deletions.
9 changes: 9 additions & 0 deletions src/plugins/management/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

export { ManagementAppLocator } from './locator';
14 changes: 7 additions & 7 deletions src/plugins/management/common/locator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@
*/

import { MANAGEMENT_APP_ID } from './contants';
import { ManagementAppLocator, MANAGEMENT_APP_LOCATOR } from './locator';
import { ManagementAppLocatorDefinition, MANAGEMENT_APP_LOCATOR } from './locator';

test('locator has the right ID', () => {
const locator = new ManagementAppLocator();
const locator = new ManagementAppLocatorDefinition();

expect(locator.id).toBe(MANAGEMENT_APP_LOCATOR);
});

test('returns management app ID', async () => {
const locator = new ManagementAppLocator();
const locator = new ManagementAppLocatorDefinition();
const location = await locator.getLocation({
sectionId: 'a',
appId: 'b',
Expand All @@ -28,26 +28,26 @@ test('returns management app ID', async () => {
});

test('returns Kibana location for section ID and app ID pair', async () => {
const locator = new ManagementAppLocator();
const locator = new ManagementAppLocatorDefinition();
const location = await locator.getLocation({
sectionId: 'ingest',
appId: 'index',
});

expect(location).toMatchObject({
route: '/ingest/index',
path: '/ingest/index',
state: {},
});
});

test('when app ID is not provided, returns path to just the section ID', async () => {
const locator = new ManagementAppLocator();
const locator = new ManagementAppLocatorDefinition();
const location = await locator.getLocation({
sectionId: 'data',
});

expect(location).toMatchObject({
route: '/data',
path: '/data',
state: {},
});
});
11 changes: 7 additions & 4 deletions src/plugins/management/common/locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import { SerializableState } from 'src/plugins/kibana_utils/common';
import { LocatorDefinition } from 'src/plugins/share/common';
import { LocatorDefinition, LocatorPublic } from 'src/plugins/share/common';
import { MANAGEMENT_APP_ID } from './contants';

export const MANAGEMENT_APP_LOCATOR = 'MANAGEMENT_APP_LOCATOR';
Expand All @@ -17,15 +17,18 @@ export interface ManagementAppLocatorParams extends SerializableState {
appId?: string;
}

export class ManagementAppLocator implements LocatorDefinition<ManagementAppLocatorParams> {
export type ManagementAppLocator = LocatorPublic<ManagementAppLocatorParams>;

export class ManagementAppLocatorDefinition
implements LocatorDefinition<ManagementAppLocatorParams> {
public readonly id = MANAGEMENT_APP_LOCATOR;

public readonly getLocation = async (params: ManagementAppLocatorParams) => {
const route = `/${params.sectionId}${params.appId ? '/' + params.appId : ''}`;
const path = `/${params.sectionId}${params.appId ? '/' + params.appId : ''}`;

return {
app: MANAGEMENT_APP_ID,
route,
path,
state: {},
};
};
Expand Down
4 changes: 3 additions & 1 deletion src/plugins/management/public/mocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ const createSetupContract = (): ManagementSetup => ({
locator: {
getLocation: jest.fn(async () => ({
app: 'MANAGEMENT',
route: '',
path: '',
state: {},
})),
getUrl: jest.fn(),
useUrl: jest.fn(),
navigate: jest.fn(),
},
});
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/management/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
} from '../../../core/public';

import { MANAGEMENT_APP_ID } from '../common/contants';
import { ManagementAppLocator } from '../common/locator';
import { ManagementAppLocatorDefinition } from '../common/locator';
import {
ManagementSectionsService,
getSectionsServiceStartPrivate,
Expand Down Expand Up @@ -74,7 +74,7 @@ export class ManagementPlugin

public setup(core: CoreSetup, { home, share }: ManagementSetupDependencies) {
const kibanaVersion = this.initializerContext.env.packageInfo.version;
const locator = share.url.locators.create(new ManagementAppLocator());
const locator = share.url.locators.create(new ManagementAppLocatorDefinition());

if (home) {
home.featureCatalogue.register({
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/management/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from 'kibana/server';
import { LocatorPublic } from 'src/plugins/share/common';
import type { SharePluginSetup } from 'src/plugins/share/server';
import { ManagementAppLocator, ManagementAppLocatorParams } from '../common/locator';
import { ManagementAppLocatorDefinition, ManagementAppLocatorParams } from '../common/locator';
import { capabilitiesProvider } from './capabilities_provider';

interface ManagementSetupDependencies {
Expand All @@ -31,7 +31,7 @@ export class ManagementServerPlugin
public setup(core: CoreSetup, { share }: ManagementSetupDependencies) {
this.logger.debug('management: Setup');

const locator = share.url.locators.create(new ManagementAppLocator());
const locator = share.url.locators.create(new ManagementAppLocatorDefinition());

core.capabilities.registerProvider(capabilitiesProvider);

Expand Down
2 changes: 1 addition & 1 deletion src/plugins/share/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
* Side Public License, v 1.
*/

export { LocatorDefinition, LocatorPublic } from './url_service';
export { LocatorDefinition, LocatorPublic, useLocatorUrl } from './url_service';
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe('locators', () => {

expect(location).toEqual({
app: 'test_app',
route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=21',
path: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=21',
state: { isFlyoutOpen: true },
});
});
Expand Down Expand Up @@ -97,7 +97,7 @@ describe('locators', () => {
expect(deps.navigate).toHaveBeenCalledWith(
{
app: 'test_app',
route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1',
path: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1',
state: {
isFlyoutOpen: false,
},
Expand Down Expand Up @@ -130,7 +130,7 @@ describe('locators', () => {
expect(deps.navigate).toHaveBeenCalledWith(
{
app: 'test_app',
route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1',
path: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1',
state: {
isFlyoutOpen: false,
},
Expand All @@ -153,7 +153,7 @@ describe('locators', () => {
expect(deps.navigate).toHaveBeenCalledWith(
{
app: 'test_app',
route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=2',
path: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=2',
state: {
isFlyoutOpen: false,
},
Expand Down
5 changes: 4 additions & 1 deletion src/plugins/share/common/url_service/__tests__/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const testLocator: LocatorDefinition<TestLocatorState> = {
getLocation: async ({ savedObjectId, pageNumber, showFlyout }) => {
return {
app: 'test_app',
route: `/my-object/${savedObjectId}?page=${pageNumber}`,
path: `/my-object/${savedObjectId}?page=${pageNumber}`,
state: {
isFlyoutOpen: showFlyout,
},
Expand All @@ -34,6 +34,9 @@ export const urlServiceTestSetup = (partialDeps: Partial<UrlServiceDependencies>
navigate: async () => {
throw new Error('not implemented');
},
getUrl: async () => {
throw new Error('not implemented');
},
...partialDeps,
};
const service = new UrlService(deps);
Expand Down
1 change: 1 addition & 0 deletions src/plugins/share/common/url_service/locators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
export * from './types';
export * from './locator';
export * from './locator_client';
export { useLocatorUrl } from './use_locator_url';
27 changes: 27 additions & 0 deletions src/plugins/share/common/url_service/locators/locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,27 @@
*/

import type { SavedObjectReference } from 'kibana/server';
import { DependencyList } from 'react';
import type { PersistableState, SerializableState } from 'src/plugins/kibana_utils/common';
import { useLocatorUrl } from './use_locator_url';
import type {
LocatorDefinition,
LocatorPublic,
KibanaLocation,
LocatorNavigationParams,
LocatorGetUrlParams,
} from './types';

export interface LocatorDependencies {
/**
* Navigate without reloading the page to a KibanaLocation.
*/
navigate: (location: KibanaLocation, params?: LocatorNavigationParams) => Promise<void>;

/**
* Resolve a Kibana URL given KibanaLocation.
*/
getUrl: (location: KibanaLocation, getUrlParams: LocatorGetUrlParams) => Promise<string>;
}

export class Locator<P extends SerializableState> implements PersistableState<P>, LocatorPublic<P> {
Expand Down Expand Up @@ -57,13 +68,29 @@ export class Locator<P extends SerializableState> implements PersistableState<P>
return await this.definition.getLocation(params);
}

public async getUrl(params: P, { absolute = false }: LocatorGetUrlParams = {}): Promise<string> {
const location = await this.getLocation(params);
const url = this.deps.getUrl(location, { absolute });

return url;
}

public async navigate(
params: P,
{ replace = false }: LocatorNavigationParams = {}
): Promise<void> {
const location = await this.getLocation(params);

await this.deps.navigate(location, {
replace,
});
}

/* eslint-disable react-hooks/rules-of-hooks */
public readonly useUrl = (
params: P,
getUrlParams?: LocatorGetUrlParams,
deps: DependencyList = []
): string => useLocatorUrl<P>(this, params, getUrlParams, deps);
/* eslint-enable react-hooks/rules-of-hooks */
}
43 changes: 39 additions & 4 deletions src/plugins/share/common/url_service/locators/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* Side Public License, v 1.
*/

import { DependencyList } from 'react';
import { PersistableState, SerializableState } from 'src/plugins/kibana_utils/common';

/**
Expand Down Expand Up @@ -51,23 +52,57 @@ export interface LocatorDefinition<P extends SerializableState>
*/
export interface LocatorPublic<P> {
/**
* Returns a relative URL to the client-side redirect endpoint using this
* locator. (This method is necessary for compatibility with URL generators.)
* Returns a reference to a Kibana client-side location.
*
* @param params URL locator parameters.
*/
getLocation(params: P): Promise<KibanaLocation>;

/**
* Returns a URL as a string.
*
* @param params URL locator parameters.
* @param getUrlParams URL construction parameters.
*/
getUrl(params: P, getUrlParams?: LocatorGetUrlParams): Promise<string>;

/**
* Navigate using the `core.application.navigateToApp()` method to a Kibana
* location generated by this locator. This method is available only on the
* browser.
*
* @param params URL locator parameters.
* @param navigationParams Navigation parameters.
*/
navigate(params: P, navigationParams?: LocatorNavigationParams): Promise<void>;

/**
* React hook which returns a URL string given locator parameters. Returns
* empty string if URL is being loaded or an error happened.
*/
useUrl: (params: P, getUrlParams?: LocatorGetUrlParams, deps?: DependencyList) => string;
}

/**
* Parameters used when navigating on client-side using browser history object.
*/
export interface LocatorNavigationParams {
/**
* Whether to replace a navigation entry in history queue or push a new entry.
*/
replace?: boolean;
}

/**
* Parameters used when constructing a string URL.
*/
export interface LocatorGetUrlParams {
/**
* Whether to return an absolute long URL or relative short URL.
*/
absolute?: boolean;
}

/**
* This interface represents a location in Kibana to which one can navigate
* using the `core.application.navigateToApp()` method.
Expand All @@ -79,9 +114,9 @@ export interface KibanaLocation<S = object> {
app: string;

/**
* A URL route within a Kibana application.
* A relative URL path within a Kibana application.
*/
route: string;
path: string;

/**
* A serializable location state object, which the app can use to determine
Expand Down
46 changes: 46 additions & 0 deletions src/plugins/share/common/url_service/locators/use_locator_url.ts
Original file line number Diff line number Diff line change
@@ -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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { DependencyList, useEffect, useState } from 'react';
import useMountedState from 'react-use/lib/useMountedState';
import { SerializableState } from 'src/plugins/kibana_utils/common';
import { LocatorGetUrlParams, LocatorPublic } from '../../../common/url_service';

export const useLocatorUrl = <P extends SerializableState>(
locator: LocatorPublic<P> | null | undefined,
params: P,
getUrlParams?: LocatorGetUrlParams,
deps: DependencyList = []
): string => {
const [url, setUrl] = useState<string>('');
const isMounted = useMountedState();

/* eslint-disable react-hooks/exhaustive-deps */
useEffect(() => {
if (!locator) {
setUrl('');
return;
}

locator
.getUrl(params, getUrlParams)
.then((result: string) => {
if (!isMounted()) return;
setUrl(result);
})
.catch((error) => {
if (!isMounted()) return;
// eslint-disable-next-line no-console
console.error('useLocatorUrl', error);
setUrl('');
});
}, [locator, ...deps]);
/* eslint-enable react-hooks/exhaustive-deps */

return url;
};
Loading

0 comments on commit 1fb2640

Please sign in to comment.