Skip to content

Commit

Permalink
Url service locators (#101045) (#101473)
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

* test: ๐Ÿ’ make test locator async

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
streamich and kibanamachine committed Jun 7, 2021
1 parent ce25bf2 commit ae2fe27
Show file tree
Hide file tree
Showing 13 changed files with 553 additions and 20 deletions.
14 changes: 5 additions & 9 deletions src/plugins/discover/public/url_generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,10 @@
* Side Public License, v 1.
*/

import {
TimeRange,
Filter,
Query,
esFilters,
QueryState,
RefreshInterval,
} from '../../data/public';
import type { UrlGeneratorsDefinition } from '../../share/public';
import type { TimeRange, Filter, Query, QueryState, RefreshInterval } from '../../data/public';
import { esFilters } from '../../data/public';
import { setStateToKbnUrl } from '../../kibana_utils/public';
import { UrlGeneratorsDefinition } from '../../share/public';

export const DISCOVER_APP_URL_GENERATOR = 'DISCOVER_APP_URL_GENERATOR';

Expand Down Expand Up @@ -71,10 +65,12 @@ export interface DiscoverUrlGeneratorState {
* Used interval of the histogram
*/
interval?: string;

/**
* Array of the used sorting [[field,direction],...]
*/
sort?: string[][];

/**
* id of the used saved query
*/
Expand Down
165 changes: 165 additions & 0 deletions src/plugins/share/common/url_service/__tests__/locators.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* 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 { of } from 'src/plugins/kibana_utils/common';
import { testLocator, TestLocatorState, urlServiceTestSetup } from './setup';

describe('locators', () => {
test('can start locators service', () => {
const {
service: { locators },
} = urlServiceTestSetup();

expect(typeof locators).toBe('object');
expect(typeof locators.create).toBe('function');
expect(typeof locators.get).toBe('function');
});

test('returns "undefined" for unregistered locator', () => {
const {
service: { locators },
} = urlServiceTestSetup();

expect(locators.get(testLocator.id)).toBe(undefined);
});

test('can register a locator', () => {
const {
service: { locators },
} = urlServiceTestSetup();

locators.create(testLocator);
expect(typeof locators.get(testLocator.id)).toBe('object');
});

test('getLocation() returns KibanaLocation generated by the locator', async () => {
const {
service: { locators },
} = urlServiceTestSetup();

locators.create(testLocator);

const locator = locators.get<TestLocatorState>(testLocator.id);
const location = await locator?.getLocation({
savedObjectId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
pageNumber: 21,
showFlyout: true,
});

expect(location).toEqual({
app: 'test_app',
route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=21',
state: { isFlyoutOpen: true },
});
});

describe('.navigate()', () => {
test('throws if navigation method is not implemented', async () => {
const {
service: { locators },
} = urlServiceTestSetup();
const locator = locators.create(testLocator);
const [, error] = await of(
locator.navigate({
pageNumber: 1,
savedObjectId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
showFlyout: false,
})
);

expect(error).toBeInstanceOf(Error);
expect(error.message).toBe('not implemented');
});

test('navigates user when .navigate() method is called', async () => {
const {
service: { locators },
deps,
} = urlServiceTestSetup({
navigate: jest.fn(async () => {}),
});
const locator = locators.create(testLocator);
const [, error] = await of(
locator.navigate({
pageNumber: 1,
savedObjectId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
showFlyout: false,
})
);

expect(error).toBe(undefined);
expect(deps.navigate).toHaveBeenCalledTimes(1);
expect(deps.navigate).toHaveBeenCalledWith(
{
app: 'test_app',
route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1',
state: {
isFlyoutOpen: false,
},
},
{ replace: false }
);
});

test('can specify "replace" navigation parameter', async () => {
const {
service: { locators },
deps,
} = urlServiceTestSetup({
navigate: jest.fn(async () => {}),
});
const locator = locators.create(testLocator);

await locator.navigate(
{
pageNumber: 1,
savedObjectId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
showFlyout: false,
},
{
replace: false,
}
);

expect(deps.navigate).toHaveBeenCalledTimes(1);
expect(deps.navigate).toHaveBeenCalledWith(
{
app: 'test_app',
route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1',
state: {
isFlyoutOpen: false,
},
},
{ replace: false }
);

await locator.navigate(
{
pageNumber: 2,
savedObjectId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
showFlyout: false,
},
{
replace: true,
}
);

expect(deps.navigate).toHaveBeenCalledTimes(2);
expect(deps.navigate).toHaveBeenCalledWith(
{
app: 'test_app',
route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=2',
state: {
isFlyoutOpen: false,
},
},
{ replace: true }
);
});
});
});
42 changes: 42 additions & 0 deletions src/plugins/share/common/url_service/__tests__/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import type { SerializableState } from 'src/plugins/kibana_utils/common';
import { LocatorDefinition } from '../locators';
import { UrlService, UrlServiceDependencies } from '../url_service';

export interface TestLocatorState extends SerializableState {
savedObjectId: string;
showFlyout: boolean;
pageNumber: number;
}

export const testLocator: LocatorDefinition<TestLocatorState> = {
id: 'TEST_LOCATOR',
getLocation: async ({ savedObjectId, pageNumber, showFlyout }) => {
return {
app: 'test_app',
route: `/my-object/${savedObjectId}?page=${pageNumber}`,
state: {
isFlyoutOpen: showFlyout,
},
};
},
};

export const urlServiceTestSetup = (partialDeps: Partial<UrlServiceDependencies> = {}) => {
const deps: UrlServiceDependencies = {
navigate: async () => {
throw new Error('not implemented');
},
...partialDeps,
};
const service = new UrlService(deps);

return { service, deps };
};
10 changes: 10 additions & 0 deletions src/plugins/share/common/url_service/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* 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 * from './url_service';
export * from './locators';
11 changes: 11 additions & 0 deletions src/plugins/share/common/url_service/locators/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* 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 * from './types';
export * from './locator';
export * from './locator_client';
69 changes: 69 additions & 0 deletions src/plugins/share/common/url_service/locators/locator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

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

export interface LocatorDependencies {
navigate: (location: KibanaLocation, params?: LocatorNavigationParams) => Promise<void>;
}

export class Locator<P extends SerializableState> implements PersistableState<P>, LocatorPublic<P> {
public readonly migrations: PersistableState<P>['migrations'];

constructor(
public readonly definition: LocatorDefinition<P>,
protected readonly deps: LocatorDependencies
) {
this.migrations = definition.migrations || {};
}

// PersistableState<P> -------------------------------------------------------

public readonly telemetry: PersistableState<P>['telemetry'] = (
state: P,
stats: Record<string, any>
): Record<string, any> => {
return this.definition.telemetry ? this.definition.telemetry(state, stats) : stats;
};

public readonly inject: PersistableState<P>['inject'] = (
state: P,
references: SavedObjectReference[]
): P => {
return this.definition.inject ? this.definition.inject(state, references) : state;
};

public readonly extract: PersistableState<P>['extract'] = (
state: P
): { state: P; references: SavedObjectReference[] } => {
return this.definition.extract ? this.definition.extract(state) : { state, references: [] };
};

// LocatorPublic<P> ----------------------------------------------------------

public async getLocation(params: P): Promise<KibanaLocation> {
return await this.definition.getLocation(params);
}

public async navigate(
params: P,
{ replace = false }: LocatorNavigationParams = {}
): Promise<void> {
const location = await this.getLocation(params);
await this.deps.navigate(location, {
replace,
});
}
}
47 changes: 47 additions & 0 deletions src/plugins/share/common/url_service/locators/locator_client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import type { SerializableState } from 'src/plugins/kibana_utils/common';
import type { LocatorDependencies } from './locator';
import type { LocatorDefinition, LocatorPublic, ILocatorClient } from './types';
import { Locator } from './locator';

export type LocatorClientDependencies = LocatorDependencies;

export class LocatorClient implements ILocatorClient {
/**
* Collection of registered locators.
*/
protected locators: Map<string, Locator<any>> = new Map();

constructor(protected readonly deps: LocatorClientDependencies) {}

/**
* Creates and register a URL locator.
*
* @param definition A definition of URL locator.
* @returns A public interface of URL locator.
*/
public create<P extends SerializableState>(definition: LocatorDefinition<P>): LocatorPublic<P> {
const locator = new Locator<P>(definition, this.deps);

this.locators.set(definition.id, locator);

return locator;
}

/**
* Returns a previously registered URL locator.
*
* @param id ID of a URL locator.
* @returns A public interface of a registered URL locator.
*/
public get<P>(id: string): undefined | LocatorPublic<P> {
return this.locators.get(id);
}
}
Loading

0 comments on commit ae2fe27

Please sign in to comment.