mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
Locator docs (#103129)
* feat: 🎸 add locator_examples plugin * feat: 🎸 add example app in locator_examples * feat: 🎸 add locator_explorer plugin * chore: 🤖 remove url_generaotrs_* example plugins * docs: ✏️ update share plugin readme * docs: ✏️ add locators readme * docs: ✏️ update docs link in example plugin * docs: ✏️ update navigation docs * fix: 🐛 make P extend SerializableState * test: 💍 update test mocks * fix: 🐛 use correct type in ingest pipeline locator * test: 💍 add missing methods in mock * test: 💍 update test mocks * chore: 🤖 update plugin list Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
64df69890d
commit
82e32faf1a
28 changed files with 380 additions and 188 deletions
|
@ -47,24 +47,26 @@ console.log(discoverUrl); // http://localhost:5601/bpr/s/space/app/discover
|
||||||
const discoverUrlWithSomeState = core.http.basePath.prepend(`/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:'2020-09-10T11:39:50.203Z',to:'2020-09-10T11:40:20.249Z'))&_a=(columns:!(_source),filters:!(),index:'90943e30-9a47-11e8-b64d-95841ca0b247',interval:auto,query:(language:kuery,query:''),sort:!())`);
|
const discoverUrlWithSomeState = core.http.basePath.prepend(`/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:'2020-09-10T11:39:50.203Z',to:'2020-09-10T11:40:20.249Z'))&_a=(columns:!(_source),filters:!(),index:'90943e30-9a47-11e8-b64d-95841ca0b247',interval:auto,query:(language:kuery,query:''),sort:!())`);
|
||||||
----
|
----
|
||||||
|
|
||||||
Instead, each app should expose {kib-repo}tree/{branch}/src/plugins/share/public/url_generators/README.md[a URL generator].
|
Instead, each app should expose {kib-repo}tree/{branch}/src/plugins/share/common/url_service/locators/README.md[a locator].
|
||||||
Other apps should use those URL generators for creating URLs.
|
Other apps should use those locators for navigation or URL creation.
|
||||||
|
|
||||||
[source,typescript jsx]
|
[source,typescript jsx]
|
||||||
----
|
----
|
||||||
// Properly generated URL to *Discover* app. Generator code is owned by *Discover* app and available on *Discover*'s plugin contract.
|
// Properly generated URL to *Discover* app. Locator code is owned by *Discover* app and available on *Discover*'s plugin contract.
|
||||||
const discoverUrl = discoverUrlGenerator.createUrl({filters, timeRange});
|
const discoverUrl = await plugins.discover.locator.getUrl({filters, timeRange});
|
||||||
|
// or directly execute navigation
|
||||||
|
await plugins.discover.locator.navigate({filters, timeRange});
|
||||||
----
|
----
|
||||||
|
|
||||||
To get a better idea, take a look at *Discover* URL generator {kib-repo}tree/{branch}/src/plugins/discover/public/url_generator.ts[implementation].
|
To get a better idea, take a look at *Discover* locator {kib-repo}tree/{branch}/src/plugins/discover/public/locator.ts[implementation].
|
||||||
It allows specifying various **Discover** app state pieces like: index pattern, filters, query, time range and more.
|
It allows specifying various **Discover** app state pieces like: index pattern, filters, query, time range and more.
|
||||||
|
|
||||||
There are two ways to access other's app URL generator in your code:
|
There are two ways to access locators of other apps:
|
||||||
|
|
||||||
1. From a plugin contract of a destination app *(preferred)*.
|
1. From a plugin contract of a destination app *(preferred)*.
|
||||||
2. Using URL generator service instance on `share` plugin contract (in case an explicit plugin dependency is not possible).
|
2. Using locator client in `share` plugin (case an explicit plugin dependency is not possible).
|
||||||
|
|
||||||
In case you want other apps to link to your app, then you should create a URL generator and expose it on your plugin's contract.
|
In case you want other apps to link to your app, then you should create a locator and expose it on your plugin's contract.
|
||||||
|
|
||||||
|
|
||||||
[[navigating-between-kibana-apps]]
|
[[navigating-between-kibana-apps]]
|
||||||
|
|
|
@ -191,7 +191,8 @@ so they can properly protect the data within their clusters.
|
||||||
|
|
||||||
|
|
||||||
|{kib-repo}blob/{branch}/src/plugins/share/README.md[share]
|
|{kib-repo}blob/{branch}/src/plugins/share/README.md[share]
|
||||||
|Replaces the legacy ui/share module for registering share context menus.
|
|The share plugin contains various utilities for displaying sharing context menu,
|
||||||
|
generating deep links to other apps, and creating short URLs.
|
||||||
|
|
||||||
|
|
||||||
|{kib-repo}blob/{branch}/src/plugins/spaces_oss/README.md[spacesOss]
|
|{kib-repo}blob/{branch}/src/plugins/spaces_oss/README.md[spacesOss]
|
||||||
|
|
8
examples/locator_examples/README.md
Normal file
8
examples/locator_examples/README.md
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# Locator examples
|
||||||
|
|
||||||
|
This example plugin shows how to:
|
||||||
|
|
||||||
|
- Register a URL locator.
|
||||||
|
- Return locator from plugin contract.
|
||||||
|
|
||||||
|
To run this example, use the command `yarn start --run-examples`. Navigate to the locator app.
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"id": "urlGeneratorsExamples",
|
"id": "locatorExamples",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"kibanaVersion": "kibana",
|
"kibanaVersion": "kibana",
|
||||||
"server": false,
|
"server": false,
|
||||||
|
@ -7,6 +7,6 @@
|
||||||
"requiredPlugins": ["share"],
|
"requiredPlugins": ["share"],
|
||||||
"optionalPlugins": [],
|
"optionalPlugins": [],
|
||||||
"extraPublicDirs": [
|
"extraPublicDirs": [
|
||||||
"public/url_generator"
|
"public/locator"
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -6,6 +6,14 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AccessLinksExplorerPlugin } from './plugin';
|
import { LocatorExamplesPlugin } from './plugin';
|
||||||
|
|
||||||
export const plugin = () => new AccessLinksExplorerPlugin();
|
export {
|
||||||
|
HelloLocator,
|
||||||
|
HelloLocatorV1Params,
|
||||||
|
HelloLocatorV2Params,
|
||||||
|
HelloLocatorParams,
|
||||||
|
HELLO_LOCATOR,
|
||||||
|
} from './locator';
|
||||||
|
|
||||||
|
export const plugin = () => new LocatorExamplesPlugin();
|
54
examples/locator_examples/public/locator.ts
Normal file
54
examples/locator_examples/public/locator.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* 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 { SerializableState, MigrateFunction } from 'src/plugins/kibana_utils/common';
|
||||||
|
import { LocatorDefinition, LocatorPublic } from '../../../src/plugins/share/public';
|
||||||
|
|
||||||
|
export const HELLO_LOCATOR = 'HELLO_LOCATOR';
|
||||||
|
|
||||||
|
export interface HelloLocatorV1Params extends SerializableState {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HelloLocatorV2Params extends SerializableState {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HelloLocatorParams = HelloLocatorV2Params;
|
||||||
|
|
||||||
|
const migrateV1ToV2: MigrateFunction<HelloLocatorV1Params, HelloLocatorV2Params> = (
|
||||||
|
v1: HelloLocatorV1Params
|
||||||
|
) => {
|
||||||
|
const v2: HelloLocatorV2Params = {
|
||||||
|
firstName: v1.name,
|
||||||
|
lastName: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
return v2;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HelloLocator = LocatorPublic<HelloLocatorParams>;
|
||||||
|
|
||||||
|
export class HelloLocatorDefinition implements LocatorDefinition<HelloLocatorParams> {
|
||||||
|
public readonly id = HELLO_LOCATOR;
|
||||||
|
|
||||||
|
public readonly getLocation = async ({ firstName, lastName }: HelloLocatorParams) => {
|
||||||
|
return {
|
||||||
|
app: 'locatorExamples',
|
||||||
|
path: `/hello?firstName=${encodeURIComponent(firstName)}&lastName=${encodeURIComponent(
|
||||||
|
lastName
|
||||||
|
)}`,
|
||||||
|
state: {},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
public readonly migrations = {
|
||||||
|
'0.0.2': (migrateV1ToV2 as unknown) as MigrateFunction,
|
||||||
|
};
|
||||||
|
}
|
|
@ -8,44 +8,27 @@
|
||||||
|
|
||||||
import { SharePluginStart, SharePluginSetup } from '../../../src/plugins/share/public';
|
import { SharePluginStart, SharePluginSetup } from '../../../src/plugins/share/public';
|
||||||
import { Plugin, CoreSetup, AppMountParameters, AppNavLinkStatus } from '../../../src/core/public';
|
import { Plugin, CoreSetup, AppMountParameters, AppNavLinkStatus } from '../../../src/core/public';
|
||||||
import {
|
import { HelloLocator, HelloLocatorDefinition } from './locator';
|
||||||
HelloLinkGeneratorState,
|
|
||||||
createHelloPageLinkGenerator,
|
|
||||||
LegacyHelloLinkGeneratorState,
|
|
||||||
HELLO_URL_GENERATOR_V1,
|
|
||||||
HELLO_URL_GENERATOR,
|
|
||||||
helloPageLinkGeneratorV1,
|
|
||||||
} from './url_generator';
|
|
||||||
|
|
||||||
declare module '../../../src/plugins/share/public' {
|
interface SetupDeps {
|
||||||
export interface UrlGeneratorStateMapping {
|
share: SharePluginSetup;
|
||||||
[HELLO_URL_GENERATOR_V1]: LegacyHelloLinkGeneratorState;
|
|
||||||
[HELLO_URL_GENERATOR]: HelloLinkGeneratorState;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StartDeps {
|
interface StartDeps {
|
||||||
share: SharePluginStart;
|
share: SharePluginStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SetupDeps {
|
export interface LocatorExamplesSetup {
|
||||||
share: SharePluginSetup;
|
locator: HelloLocator;
|
||||||
}
|
}
|
||||||
|
|
||||||
const APP_ID = 'urlGeneratorsExamples';
|
export class LocatorExamplesPlugin
|
||||||
|
implements Plugin<LocatorExamplesSetup, void, SetupDeps, StartDeps> {
|
||||||
export class AccessLinksExamplesPlugin implements Plugin<void, void, SetupDeps, StartDeps> {
|
public setup(core: CoreSetup<StartDeps>, plugins: SetupDeps) {
|
||||||
public setup(core: CoreSetup<StartDeps>, { share: { urlGenerators } }: SetupDeps) {
|
const locator = plugins.share.url.locators.create(new HelloLocatorDefinition());
|
||||||
urlGenerators.registerUrlGenerator(
|
|
||||||
createHelloPageLinkGenerator(async () => ({
|
|
||||||
appBasePath: (await core.getStartServices())[0].application.getUrlForApp(APP_ID),
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
urlGenerators.registerUrlGenerator(helloPageLinkGeneratorV1);
|
|
||||||
|
|
||||||
core.application.register({
|
core.application.register({
|
||||||
id: APP_ID,
|
id: 'locatorExamples',
|
||||||
title: 'Access links examples',
|
title: 'Access links examples',
|
||||||
navLinkStatus: AppNavLinkStatus.hidden,
|
navLinkStatus: AppNavLinkStatus.hidden,
|
||||||
async mount(params: AppMountParameters) {
|
async mount(params: AppMountParameters) {
|
||||||
|
@ -58,6 +41,10 @@ export class AccessLinksExamplesPlugin implements Plugin<void, void, SetupDeps,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
locator,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public start() {}
|
public start() {}
|
|
@ -1,6 +1,7 @@
|
||||||
## Access links explorer
|
# Locator explorer
|
||||||
|
|
||||||
|
This example plugin shows how to:
|
||||||
|
|
||||||
This example app shows how to:
|
|
||||||
- Generate links to other applications
|
- Generate links to other applications
|
||||||
- Generate dynamic links, when the target application is not known
|
- Generate dynamic links, when the target application is not known
|
||||||
- Handle backward compatibility of urls
|
- Handle backward compatibility of urls
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"id": "urlGeneratorsExplorer",
|
"id": "locatorExplorer",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"kibanaVersion": "kibana",
|
"kibanaVersion": "kibana",
|
||||||
"server": false,
|
"server": false,
|
||||||
"ui": true,
|
"ui": true,
|
||||||
"requiredPlugins": ["share", "urlGeneratorsExamples", "developerExamples"],
|
"requiredPlugins": ["share", "locatorExamples", "developerExamples"],
|
||||||
"optionalPlugins": []
|
"optionalPlugins": []
|
||||||
}
|
}
|
|
@ -8,9 +8,7 @@
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
import { EuiPage } from '@elastic/eui';
|
import { EuiPage } from '@elastic/eui';
|
||||||
|
|
||||||
import { EuiButton } from '@elastic/eui';
|
import { EuiButton } from '@elastic/eui';
|
||||||
import { EuiPageBody } from '@elastic/eui';
|
import { EuiPageBody } from '@elastic/eui';
|
||||||
import { EuiPageContent } from '@elastic/eui';
|
import { EuiPageContent } from '@elastic/eui';
|
||||||
|
@ -21,44 +19,54 @@ import { EuiFieldText } from '@elastic/eui';
|
||||||
import { EuiPageHeader } from '@elastic/eui';
|
import { EuiPageHeader } from '@elastic/eui';
|
||||||
import { EuiLink } from '@elastic/eui';
|
import { EuiLink } from '@elastic/eui';
|
||||||
import { AppMountParameters } from '../../../src/core/public';
|
import { AppMountParameters } from '../../../src/core/public';
|
||||||
import { UrlGeneratorsService } from '../../../src/plugins/share/public';
|
import { SharePluginSetup } from '../../../src/plugins/share/public';
|
||||||
import {
|
import {
|
||||||
HELLO_URL_GENERATOR,
|
HelloLocatorV1Params,
|
||||||
HELLO_URL_GENERATOR_V1,
|
HelloLocatorV2Params,
|
||||||
} from '../../url_generators_examples/public/url_generator';
|
HELLO_LOCATOR,
|
||||||
|
} from '../../locator_examples/public';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
getLinkGenerator: UrlGeneratorsService['getUrlGenerator'];
|
share: SharePluginSetup;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MigratedLink {
|
interface MigratedLink {
|
||||||
isDeprecated: boolean;
|
|
||||||
linkText: string;
|
linkText: string;
|
||||||
link: string;
|
link: string;
|
||||||
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ActionsExplorer = ({ getLinkGenerator }: Props) => {
|
const ActionsExplorer = ({ share }: Props) => {
|
||||||
const [migratedLinks, setMigratedLinks] = useState([] as MigratedLink[]);
|
const [migratedLinks, setMigratedLinks] = useState([] as MigratedLink[]);
|
||||||
const [buildingLinks, setBuildingLinks] = useState(false);
|
const [buildingLinks, setBuildingLinks] = useState(false);
|
||||||
const [firstName, setFirstName] = useState('');
|
const [firstName, setFirstName] = useState('');
|
||||||
const [lastName, setLastName] = useState('');
|
const [lastName, setLastName] = useState('');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lets pretend we grabbed these links from a persistent store, like a saved object.
|
* Lets pretend we grabbed these links from a persistent store, like a saved object.
|
||||||
* Some of these links were created with older versions of the hello link generator.
|
* Some of these links were created with older versions of the hello locator.
|
||||||
* They use deprecated generator ids.
|
|
||||||
*/
|
*/
|
||||||
const [persistedLinks, setPersistedLinks] = useState([
|
const [persistedLinks, setPersistedLinks] = useState<
|
||||||
|
Array<{
|
||||||
|
id: string;
|
||||||
|
version: string;
|
||||||
|
linkText: string;
|
||||||
|
params: HelloLocatorV1Params | HelloLocatorV2Params;
|
||||||
|
}>
|
||||||
|
>([
|
||||||
{
|
{
|
||||||
id: HELLO_URL_GENERATOR_V1,
|
id: HELLO_LOCATOR,
|
||||||
|
version: '0.0.1',
|
||||||
linkText: 'Say hello to Mary',
|
linkText: 'Say hello to Mary',
|
||||||
state: {
|
params: {
|
||||||
name: 'Mary',
|
name: 'Mary',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: HELLO_URL_GENERATOR,
|
id: HELLO_LOCATOR,
|
||||||
|
version: '0.0.2',
|
||||||
linkText: 'Say hello to George',
|
linkText: 'Say hello to George',
|
||||||
state: {
|
params: {
|
||||||
firstName: 'George',
|
firstName: 'George',
|
||||||
lastName: 'Washington',
|
lastName: 'Washington',
|
||||||
},
|
},
|
||||||
|
@ -71,30 +79,38 @@ const ActionsExplorer = ({ getLinkGenerator }: Props) => {
|
||||||
const updateLinks = async () => {
|
const updateLinks = async () => {
|
||||||
const updatedLinks = await Promise.all(
|
const updatedLinks = await Promise.all(
|
||||||
persistedLinks.map(async (savedLink) => {
|
persistedLinks.map(async (savedLink) => {
|
||||||
const generator = getLinkGenerator(savedLink.id);
|
const locator = share.url.locators.get(savedLink.id);
|
||||||
const link = await generator.createUrl(savedLink.state);
|
if (!locator) return;
|
||||||
|
let params: HelloLocatorV1Params | HelloLocatorV2Params = savedLink.params;
|
||||||
|
if (savedLink.version === '0.0.1') {
|
||||||
|
const migration = locator.migrations['0.0.2'];
|
||||||
|
if (migration) {
|
||||||
|
params = migration(params) as HelloLocatorV2Params;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const link = await locator.getUrl(params, { absolute: false });
|
||||||
return {
|
return {
|
||||||
isDeprecated: generator.isDeprecated,
|
|
||||||
linkText: savedLink.linkText,
|
linkText: savedLink.linkText,
|
||||||
link,
|
link,
|
||||||
};
|
version: savedLink.version,
|
||||||
|
} as MigratedLink;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
setMigratedLinks(updatedLinks);
|
setMigratedLinks(updatedLinks as MigratedLink[]);
|
||||||
setBuildingLinks(false);
|
setBuildingLinks(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
updateLinks();
|
updateLinks();
|
||||||
}, [getLinkGenerator, persistedLinks]);
|
}, [share, persistedLinks]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiPage>
|
<EuiPage>
|
||||||
<EuiPageBody>
|
<EuiPageBody>
|
||||||
<EuiPageHeader>Access links explorer</EuiPageHeader>
|
<EuiPageHeader>Locator explorer</EuiPageHeader>
|
||||||
<EuiPageContent>
|
<EuiPageContent>
|
||||||
<EuiPageContentBody>
|
<EuiPageContentBody>
|
||||||
<EuiText>
|
<EuiText>
|
||||||
<p>Create new links using the most recent version of a url generator.</p>
|
<p>Create new links using the most recent version of a locator.</p>
|
||||||
</EuiText>
|
</EuiText>
|
||||||
<EuiFieldText
|
<EuiFieldText
|
||||||
prepend="First name"
|
prepend="First name"
|
||||||
|
@ -108,8 +124,9 @@ const ActionsExplorer = ({ getLinkGenerator }: Props) => {
|
||||||
setPersistedLinks([
|
setPersistedLinks([
|
||||||
...persistedLinks,
|
...persistedLinks,
|
||||||
{
|
{
|
||||||
id: HELLO_URL_GENERATOR,
|
id: HELLO_LOCATOR,
|
||||||
state: { firstName, lastName },
|
version: '0.0.2',
|
||||||
|
params: { firstName, lastName },
|
||||||
linkText: `Say hello to ${firstName} ${lastName}`,
|
linkText: `Say hello to ${firstName} ${lastName}`,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
@ -122,10 +139,10 @@ const ActionsExplorer = ({ getLinkGenerator }: Props) => {
|
||||||
<EuiText>
|
<EuiText>
|
||||||
<p>
|
<p>
|
||||||
Existing links retrieved from storage. The links that were generated from legacy
|
Existing links retrieved from storage. The links that were generated from legacy
|
||||||
generators are in red. This can be useful for developers to know they will have to
|
locators are in red. This can be useful for developers to know they will have to
|
||||||
migrate persisted state or in a future version of Kibana, these links may no longer
|
migrate persisted state or in a future version of Kibana, these links may no longer
|
||||||
work. They still work now because legacy url generators must provide a state
|
work. They still work now because legacy locators must provide state migration
|
||||||
migration function.
|
functions.
|
||||||
</p>
|
</p>
|
||||||
</EuiText>
|
</EuiText>
|
||||||
{buildingLinks ? (
|
{buildingLinks ? (
|
||||||
|
@ -134,7 +151,7 @@ const ActionsExplorer = ({ getLinkGenerator }: Props) => {
|
||||||
migratedLinks.map((link) => (
|
migratedLinks.map((link) => (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<EuiLink
|
<EuiLink
|
||||||
color={link.isDeprecated ? 'danger' : 'primary'}
|
color={link.version !== '0.0.2' ? 'danger' : 'primary'}
|
||||||
data-test-subj="linkToHelloPage"
|
data-test-subj="linkToHelloPage"
|
||||||
href={link.link}
|
href={link.link}
|
||||||
target="_blank"
|
target="_blank"
|
|
@ -6,6 +6,6 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AccessLinksExamplesPlugin } from './plugin';
|
import { LocatorExplorerPlugin } from './plugin';
|
||||||
|
|
||||||
export const plugin = () => new AccessLinksExamplesPlugin();
|
export const plugin = () => new LocatorExplorerPlugin();
|
|
@ -6,30 +6,30 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SharePluginStart } from '../../../src/plugins/share/public';
|
import { SharePluginSetup, SharePluginStart } from '../../../src/plugins/share/public';
|
||||||
import { Plugin, CoreSetup, AppMountParameters, AppNavLinkStatus } from '../../../src/core/public';
|
import { Plugin, CoreSetup, AppMountParameters, AppNavLinkStatus } from '../../../src/core/public';
|
||||||
import { DeveloperExamplesSetup } from '../../developer_examples/public';
|
import { DeveloperExamplesSetup } from '../../developer_examples/public';
|
||||||
|
|
||||||
|
interface SetupDeps {
|
||||||
|
developerExamples: DeveloperExamplesSetup;
|
||||||
|
share: SharePluginSetup;
|
||||||
|
}
|
||||||
|
|
||||||
interface StartDeps {
|
interface StartDeps {
|
||||||
share: SharePluginStart;
|
share: SharePluginStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SetupDeps {
|
export class LocatorExplorerPlugin implements Plugin<void, void, SetupDeps, StartDeps> {
|
||||||
developerExamples: DeveloperExamplesSetup;
|
public setup(core: CoreSetup<StartDeps>, { developerExamples, share }: SetupDeps) {
|
||||||
}
|
|
||||||
|
|
||||||
export class AccessLinksExplorerPlugin implements Plugin<void, void, SetupDeps, StartDeps> {
|
|
||||||
public setup(core: CoreSetup<StartDeps>, { developerExamples }: SetupDeps) {
|
|
||||||
core.application.register({
|
core.application.register({
|
||||||
id: 'urlGeneratorsExplorer',
|
id: 'locatorExplorer',
|
||||||
title: 'Access links explorer',
|
title: 'Locator explorer',
|
||||||
navLinkStatus: AppNavLinkStatus.hidden,
|
navLinkStatus: AppNavLinkStatus.hidden,
|
||||||
async mount(params: AppMountParameters) {
|
async mount(params: AppMountParameters) {
|
||||||
const depsStart = (await core.getStartServices())[1];
|
|
||||||
const { renderApp } = await import('./app');
|
const { renderApp } = await import('./app');
|
||||||
return renderApp(
|
return renderApp(
|
||||||
{
|
{
|
||||||
getLinkGenerator: depsStart.share.urlGenerators.getUrlGenerator,
|
share,
|
||||||
},
|
},
|
||||||
params
|
params
|
||||||
);
|
);
|
||||||
|
@ -37,18 +37,18 @@ export class AccessLinksExplorerPlugin implements Plugin<void, void, SetupDeps,
|
||||||
});
|
});
|
||||||
|
|
||||||
developerExamples.register({
|
developerExamples.register({
|
||||||
title: 'Url generators',
|
title: 'URL locators',
|
||||||
description: `Url generators offer are a backward compatible safe way to persist a URL in a saved object.
|
description: `Locators offer are a backward compatible safe way to persist a URL in a saved object.
|
||||||
Store the url generator id and state, instead of the href string. When the link is recreated at run time, the service
|
Store the locator ID and params, instead of the href string. When the link is recreated at run time, the service
|
||||||
will run the state through any necessary migrations. Registrators can change their URL structure
|
will run the state through any necessary migrations. Registrators can change their URL structure
|
||||||
without breaking these links stored in saved objects.
|
without breaking these links stored in saved objects.
|
||||||
`,
|
`,
|
||||||
appId: 'urlGeneratorsExplorer',
|
appId: 'locatorExplorer',
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
label: 'README',
|
label: 'README',
|
||||||
href:
|
href:
|
||||||
'https://github.com/elastic/kibana/blob/master/src/plugins/share/public/url_generators/README.md',
|
'https://github.com/elastic/kibana/blob/master/src/plugins/share/common/url_service/locators/README.md',
|
||||||
iconType: 'logoGithub',
|
iconType: 'logoGithub',
|
||||||
size: 's',
|
size: 's',
|
||||||
target: '_blank',
|
target: '_blank',
|
|
@ -1,7 +0,0 @@
|
||||||
## Access links examples
|
|
||||||
|
|
||||||
This example app shows how to:
|
|
||||||
- Register a direct access link generator.
|
|
||||||
- Handle migration of legacy generators into a new one.
|
|
||||||
|
|
||||||
To run this example, use the command `yarn start --run-examples`. Navigate to the access links explorer app
|
|
|
@ -1,68 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
|
||||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
|
||||||
* Side Public License, v 1.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import url from 'url';
|
|
||||||
import { UrlGeneratorState, UrlGeneratorsDefinition } from '../../../src/plugins/share/public';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The name of the latest variable can always stay the same so code that
|
|
||||||
* uses this link generator statically will switch to the latest version.
|
|
||||||
* Typescript will warn the developer if incorrect state is being passed
|
|
||||||
* down.
|
|
||||||
*/
|
|
||||||
export const HELLO_URL_GENERATOR = 'HELLO_URL_GENERATOR_V2';
|
|
||||||
|
|
||||||
export interface HelloLinkState {
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type HelloLinkGeneratorState = UrlGeneratorState<HelloLinkState>;
|
|
||||||
|
|
||||||
export const createHelloPageLinkGenerator = (
|
|
||||||
getStartServices: () => Promise<{ appBasePath: string }>
|
|
||||||
): UrlGeneratorsDefinition<typeof HELLO_URL_GENERATOR> => ({
|
|
||||||
id: HELLO_URL_GENERATOR,
|
|
||||||
createUrl: async (state) => {
|
|
||||||
const startServices = await getStartServices();
|
|
||||||
const appBasePath = startServices.appBasePath;
|
|
||||||
const parsedUrl = url.parse(window.location.href);
|
|
||||||
|
|
||||||
return url.format({
|
|
||||||
protocol: parsedUrl.protocol,
|
|
||||||
host: parsedUrl.host,
|
|
||||||
pathname: `${appBasePath}/hello`,
|
|
||||||
query: {
|
|
||||||
...state,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The name of this legacy generator id changes, but the *value* stays the same.
|
|
||||||
*/
|
|
||||||
export const HELLO_URL_GENERATOR_V1 = 'HELLO_URL_GENERATOR';
|
|
||||||
|
|
||||||
export interface HelloLinkStateV1 {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LegacyHelloLinkGeneratorState = UrlGeneratorState<
|
|
||||||
HelloLinkStateV1,
|
|
||||||
typeof HELLO_URL_GENERATOR,
|
|
||||||
HelloLinkState
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const helloPageLinkGeneratorV1: UrlGeneratorsDefinition<typeof HELLO_URL_GENERATOR_V1> = {
|
|
||||||
id: HELLO_URL_GENERATOR_V1,
|
|
||||||
isDeprecated: true,
|
|
||||||
migrate: async (state) => {
|
|
||||||
return { id: HELLO_URL_GENERATOR, state: { firstName: state.name, lastName: '' } };
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -21,6 +21,10 @@ const createSetupContract = (): Setup => {
|
||||||
getUrl: jest.fn(),
|
getUrl: jest.fn(),
|
||||||
useUrl: jest.fn(),
|
useUrl: jest.fn(),
|
||||||
navigate: jest.fn(),
|
navigate: jest.fn(),
|
||||||
|
extract: jest.fn(),
|
||||||
|
inject: jest.fn(),
|
||||||
|
telemetry: jest.fn(),
|
||||||
|
migrations: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return setupContract;
|
return setupContract;
|
||||||
|
@ -37,6 +41,10 @@ const createStartContract = (): Start => {
|
||||||
getUrl: jest.fn(),
|
getUrl: jest.fn(),
|
||||||
useUrl: jest.fn(),
|
useUrl: jest.fn(),
|
||||||
navigate: jest.fn(),
|
navigate: jest.fn(),
|
||||||
|
extract: jest.fn(),
|
||||||
|
inject: jest.fn(),
|
||||||
|
telemetry: jest.fn(),
|
||||||
|
migrations: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return startContract;
|
return startContract;
|
||||||
|
|
|
@ -39,6 +39,10 @@ const createSetupContract = (): ManagementSetup => ({
|
||||||
getUrl: jest.fn(),
|
getUrl: jest.fn(),
|
||||||
useUrl: jest.fn(),
|
useUrl: jest.fn(),
|
||||||
navigate: jest.fn(),
|
navigate: jest.fn(),
|
||||||
|
extract: jest.fn(),
|
||||||
|
inject: jest.fn(),
|
||||||
|
telemetry: jest.fn(),
|
||||||
|
migrations: {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,23 @@
|
||||||
# Share plugin
|
# Share plugin
|
||||||
|
|
||||||
Replaces the legacy `ui/share` module for registering share context menus.
|
The `share` plugin contains various utilities for displaying sharing context menu,
|
||||||
|
generating deep links to other apps, and creating short URLs.
|
||||||
|
|
||||||
## Example registration
|
|
||||||
|
## Sharing context menu
|
||||||
|
|
||||||
|
You can register an item into sharing context menu (which is displayed in
|
||||||
|
Dahsboard, Discover, and Visuzlize apps).
|
||||||
|
|
||||||
|
### Example registration
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// For legacy plugins
|
import { ShareContext, ShareMenuItem } from 'src/plugins/share/public';
|
||||||
import { npSetup } from 'ui/new_platform';
|
|
||||||
npSetup.plugins.share.register(/* same details here */);
|
|
||||||
|
|
||||||
// For new plugins: first add 'share' to the list of `optionalPlugins`
|
plugins.share.register({
|
||||||
// in your kibana.json file. Then access the plugin directly in `setup`:
|
id: 'MY_MENU',
|
||||||
|
getShareMenuItems: (context: ShareContext): ShareMenuItem[] => {
|
||||||
class MyPlugin {
|
// ...
|
||||||
setup(core, plugins) {
|
},
|
||||||
if (plugins.share) {
|
};
|
||||||
plugins.share.register(/* same details here. */);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that the old module supported providing a Angular DI function to receive Angular dependencies. This is no longer supported as we migrate away from Angular and will be removed in 8.0.
|
|
||||||
|
|
166
src/plugins/share/common/url_service/locators/README.md
Normal file
166
src/plugins/share/common/url_service/locators/README.md
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
# Locators
|
||||||
|
|
||||||
|
## Locators service
|
||||||
|
|
||||||
|
Developers who maintain pages in Kibana that other developers may want to link to
|
||||||
|
can register a *locator*. Locators provide backward compatibility support
|
||||||
|
so the developer of the app/page has a way to change their url structure without
|
||||||
|
breaking users of this system. If users were to generate the URLs on their own,
|
||||||
|
using string concatenation, those links may break often.
|
||||||
|
|
||||||
|
Owners: __Kibana App Services team__.
|
||||||
|
|
||||||
|
|
||||||
|
## Producer Usage
|
||||||
|
|
||||||
|
Here is how you create a *locator*, which deeply link into your Kibana app:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
plugins.share.url.locators.create({
|
||||||
|
id: 'MY_APP_LOCATOR',
|
||||||
|
getLocation: async (params: { productId: string }) => {
|
||||||
|
return {
|
||||||
|
app: 'myApp',
|
||||||
|
path: `/products/${productId}`,
|
||||||
|
state: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
When navigation in Kibana is done without a page reload a serializable *location state*
|
||||||
|
object is passed to the destination app, which the app can use to change its
|
||||||
|
appearance. The *location state* object does not appear in the URL, but apps
|
||||||
|
can still use that, similar to how URL parameters are used.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
plugins.share.url.locators.create({
|
||||||
|
id: 'MY_APP_LOCATOR',
|
||||||
|
getLocation: async (params: { productId: string, tab?: 'pics' | 'attributes' }) => {
|
||||||
|
return {
|
||||||
|
app: 'myApp',
|
||||||
|
path: `/products/${productId}`,
|
||||||
|
state: {
|
||||||
|
tab: params.tab || 'pics',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
When you want to change the shape of the parameters that the locator receives, you can
|
||||||
|
provide a migration function, which can transform the shape of the parameters from
|
||||||
|
one Kibana version to another. For example, below we replace `productId` param by `id`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
plugins.share.url.locators.create({
|
||||||
|
id: 'MY_APP_LOCATOR',
|
||||||
|
getLocation: async (params: { id: string, tab?: 'pics' | 'attributes' }) => {
|
||||||
|
return {
|
||||||
|
app: 'myApp',
|
||||||
|
path: `/products/${id}`,
|
||||||
|
state: {
|
||||||
|
tab: params.tab || 'pics',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
migrations = {
|
||||||
|
'7.20.0': ({productId, ...rest}) => {
|
||||||
|
return {
|
||||||
|
id: productId,
|
||||||
|
...rest,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The migration version should correspond to Kibana relase when the chagne was
|
||||||
|
introduced. It is the responsibility of the *consumer* to make sure they
|
||||||
|
migrate their stored parameters using the provided migration function to the
|
||||||
|
latest version. Migrations versions are ordered by semver. As a consumer,
|
||||||
|
if persist somewhere a locator parameters object, you also want to store
|
||||||
|
the version of that object, so later you know from starting from which
|
||||||
|
version you need to execute migrations.
|
||||||
|
|
||||||
|
|
||||||
|
## Consumer Usage
|
||||||
|
|
||||||
|
Consumers of the Locators service can use the locators to generate deeps links
|
||||||
|
into Kibana apps, or navigate to the apps while passing to the destination
|
||||||
|
app the *location state*.
|
||||||
|
|
||||||
|
First you will need to get hold of the *locator* for the app you want to
|
||||||
|
navigate to.
|
||||||
|
|
||||||
|
Usually, that app will return it from its plugin contract from the "setup"
|
||||||
|
life-cycle:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
class MyPlugin {
|
||||||
|
setup(core, plugins) {
|
||||||
|
const locator = plugins.destinationApp.locator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, you can get hold of any locator from the central registry:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
class MyPlugin {
|
||||||
|
setup(core, plugins) {
|
||||||
|
const locator = plugins.share.url.locators.get('DESTINATION_APP_LOCATOR');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Once you have the locator, you can use it to navigate to some kibana app:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await locator.navigate({
|
||||||
|
productId: '123',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also use it to generate a URL string of the destination:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const url = await locator.getUrl({
|
||||||
|
productId: '123',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Migrations
|
||||||
|
|
||||||
|
**As a consumer, you should not persist the resulting URL string!**
|
||||||
|
|
||||||
|
As soon as you do, you have lost your migration options. Instead you should
|
||||||
|
store the ID, version and params of your locator. This will let you
|
||||||
|
re-create the migrated URL later.
|
||||||
|
|
||||||
|
If, as a consumer, you store the ID, version and params of the locator, you
|
||||||
|
should use the migration functions provided by the locator when migrating
|
||||||
|
between Kibana versions.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const migration = locator.migrations[version];
|
||||||
|
|
||||||
|
if (migration) {
|
||||||
|
params = migration(params);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
You can view the provided example plugins to learn more how to work with locators.
|
||||||
|
There are two plugins (`locator_examples` and `locator_explorer`) provided in the
|
||||||
|
`/examples` folder. You can run the example plugins using the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn start --run-examples
|
||||||
|
```
|
||||||
|
|
||||||
|
To view the `locator_explorer` example plugin in Kibana navigate to: __Analytics__ 👉
|
||||||
|
__Developer examples__ 👉 __URL locators__.
|
|
@ -41,7 +41,7 @@ export class LocatorClient implements ILocatorClient {
|
||||||
* @param id ID of a URL locator.
|
* @param id ID of a URL locator.
|
||||||
* @returns A public interface of a registered URL locator.
|
* @returns A public interface of a registered URL locator.
|
||||||
*/
|
*/
|
||||||
public get<P>(id: string): undefined | LocatorPublic<P> {
|
public get<P extends SerializableState>(id: string): undefined | LocatorPublic<P> {
|
||||||
return this.locators.get(id);
|
return this.locators.get(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ export interface ILocatorClient {
|
||||||
*
|
*
|
||||||
* @param id Unique ID of the locator.
|
* @param id Unique ID of the locator.
|
||||||
*/
|
*/
|
||||||
get<P>(id: string): undefined | LocatorPublic<P>;
|
get<P extends SerializableState>(id: string): undefined | LocatorPublic<P>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -50,7 +50,7 @@ export interface LocatorDefinition<P extends SerializableState>
|
||||||
/**
|
/**
|
||||||
* Public interface of a registered locator.
|
* Public interface of a registered locator.
|
||||||
*/
|
*/
|
||||||
export interface LocatorPublic<P> {
|
export interface LocatorPublic<P extends SerializableState> extends PersistableState<P> {
|
||||||
/**
|
/**
|
||||||
* Returns a reference to a Kibana client-side location.
|
* Returns a reference to a Kibana client-side location.
|
||||||
*
|
*
|
||||||
|
|
|
@ -55,6 +55,10 @@ const setup = (
|
||||||
navigate: jest.fn(async () => {}),
|
navigate: jest.fn(async () => {}),
|
||||||
getUrl: jest.fn(),
|
getUrl: jest.fn(),
|
||||||
useUrl: jest.fn(),
|
useUrl: jest.fn(),
|
||||||
|
extract: jest.fn(),
|
||||||
|
inject: jest.fn(),
|
||||||
|
telemetry: jest.fn(),
|
||||||
|
migrations: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const plugins: PluginDeps = {
|
const plugins: PluginDeps = {
|
||||||
|
|
|
@ -41,6 +41,10 @@ const setup = ({ dashboardOnlyMode = false }: { dashboardOnlyMode?: boolean } =
|
||||||
navigate: jest.fn(async () => {}),
|
navigate: jest.fn(async () => {}),
|
||||||
getUrl: jest.fn(),
|
getUrl: jest.fn(),
|
||||||
useUrl: jest.fn(),
|
useUrl: jest.fn(),
|
||||||
|
extract: jest.fn(),
|
||||||
|
inject: jest.fn(),
|
||||||
|
telemetry: jest.fn(),
|
||||||
|
migrations: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const plugins: PluginDeps = {
|
const plugins: PluginDeps = {
|
||||||
|
|
|
@ -21,6 +21,10 @@ describe('Ingest pipeline locator', () => {
|
||||||
throw new Error('not implemented');
|
throw new Error('not implemented');
|
||||||
},
|
},
|
||||||
useUrl: () => '',
|
useUrl: () => '',
|
||||||
|
telemetry: jest.fn(),
|
||||||
|
extract: jest.fn(),
|
||||||
|
inject: jest.fn(),
|
||||||
|
migrations: {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return { definition };
|
return { definition };
|
||||||
|
|
|
@ -52,7 +52,7 @@ export type IngestPipelinesParams =
|
||||||
| IngestPipelinesCloneParams
|
| IngestPipelinesCloneParams
|
||||||
| IngestPipelinesCreateParams;
|
| IngestPipelinesCreateParams;
|
||||||
|
|
||||||
export type IngestPipelinesLocator = LocatorPublic<void>;
|
export type IngestPipelinesLocator = LocatorPublic<IngestPipelinesParams>;
|
||||||
|
|
||||||
export const INGEST_PIPELINES_APP_LOCATOR = 'INGEST_PIPELINES_APP_LOCATOR';
|
export const INGEST_PIPELINES_APP_LOCATOR = 'INGEST_PIPELINES_APP_LOCATOR';
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue