mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
* Add direct access link registry and dashboard impl and use in ML * Add example plugin with migration example * address code review comments * Fixes, more code review updates * Readme clean up * add tests * remove else * Rename everything from DirectAccessLinkGenerator to the much short UrlGenerator. also fix the ml # thing and return a relative link from dashboard genrator * add important text in bold * Move url generators into share plugin * add correct i18n prefix * Fix timeRange url name * make share plugin optional for dashboard * fix code owners * Use base UrlGeneratorState type, add comments * Fix hash bug and add test that would have caught it
This commit is contained in:
parent
0c12506aa7
commit
3538d5ac92
32 changed files with 1461 additions and 43 deletions
7
examples/url_generators_examples/README.md
Normal file
7
examples/url_generators_examples/README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
## 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
|
10
examples/url_generators_examples/kibana.json
Normal file
10
examples/url_generators_examples/kibana.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"id": "urlGeneratorsExamples",
|
||||
"version": "0.0.1",
|
||||
"kibanaVersion": "kibana",
|
||||
"configPath": ["url_generators_examples"],
|
||||
"server": false,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["share"],
|
||||
"optionalPlugins": []
|
||||
}
|
17
examples/url_generators_examples/package.json
Normal file
17
examples/url_generators_examples/package.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "url_generators_examples",
|
||||
"version": "1.0.0",
|
||||
"main": "target/examples/url_generators_examples",
|
||||
"kibana": {
|
||||
"version": "kibana",
|
||||
"templateVersion": "1.0.0"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"kbn": "node ../../scripts/kbn.js",
|
||||
"build": "rm -rf './target' && tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "3.5.3"
|
||||
}
|
||||
}
|
89
examples/url_generators_examples/public/app.tsx
Normal file
89
examples/url_generators_examples/public/app.tsx
Normal file
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { EuiPageBody } from '@elastic/eui';
|
||||
import { EuiPageContent } from '@elastic/eui';
|
||||
import { EuiPageContentBody } from '@elastic/eui';
|
||||
import { Route, Switch, Redirect, Router, useLocation } from 'react-router-dom';
|
||||
import { createBrowserHistory } from 'history';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import { AppMountParameters } from '../../../src/core/public';
|
||||
|
||||
function useQuery() {
|
||||
const { search } = useLocation();
|
||||
const params = React.useMemo(() => new URLSearchParams(search), [search]);
|
||||
return params;
|
||||
}
|
||||
|
||||
interface HelloPageProps {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
}
|
||||
|
||||
const HelloPage = ({ firstName, lastName }: HelloPageProps) => (
|
||||
<EuiText>{`Hello ${firstName} ${lastName}`}</EuiText>
|
||||
);
|
||||
|
||||
export const Routes: React.FC<{}> = () => {
|
||||
const query = useQuery();
|
||||
|
||||
return (
|
||||
<EuiPageBody>
|
||||
<EuiPageContent>
|
||||
<EuiPageContentBody>
|
||||
<Switch>
|
||||
<Route path="/hello">
|
||||
<HelloPage
|
||||
firstName={query.get('firstName') || ''}
|
||||
lastName={query.get('lastName') || ''}
|
||||
/>
|
||||
</Route>
|
||||
<Redirect from="/" to="/hello" />
|
||||
</Switch>
|
||||
</EuiPageContentBody>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
);
|
||||
};
|
||||
|
||||
export const LinksExample: React.FC<{
|
||||
appBasePath: string;
|
||||
}> = props => {
|
||||
const history = React.useMemo(
|
||||
() =>
|
||||
createBrowserHistory({
|
||||
basename: props.appBasePath,
|
||||
}),
|
||||
[props.appBasePath]
|
||||
);
|
||||
return (
|
||||
<Router history={history}>
|
||||
<Routes />
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
export const renderApp = (props: { appBasePath: string }, { element }: AppMountParameters) => {
|
||||
ReactDOM.render(<LinksExample {...props} />, element);
|
||||
|
||||
return () => ReactDOM.unmountComponentAtNode(element);
|
||||
};
|
22
examples/url_generators_examples/public/index.ts
Normal file
22
examples/url_generators_examples/public/index.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { AccessLinksExamplesPlugin } from './plugin';
|
||||
|
||||
export const plugin = () => new AccessLinksExamplesPlugin();
|
76
examples/url_generators_examples/public/plugin.tsx
Normal file
76
examples/url_generators_examples/public/plugin.tsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { SharePluginStart, SharePluginSetup } from '../../../src/plugins/share/public';
|
||||
import { Plugin, CoreSetup, AppMountParameters } from '../../../src/core/public';
|
||||
import {
|
||||
HelloLinkGeneratorState,
|
||||
createHelloPageLinkGenerator,
|
||||
LegacyHelloLinkGeneratorState,
|
||||
HELLO_URL_GENERATOR_V1,
|
||||
HELLO_URL_GENERATOR,
|
||||
helloPageLinkGeneratorV1,
|
||||
} from './url_generator';
|
||||
|
||||
declare module '../../../src/plugins/share/public' {
|
||||
export interface UrlGeneratorStateMapping {
|
||||
[HELLO_URL_GENERATOR_V1]: LegacyHelloLinkGeneratorState;
|
||||
[HELLO_URL_GENERATOR]: HelloLinkGeneratorState;
|
||||
}
|
||||
}
|
||||
|
||||
interface StartDeps {
|
||||
share: SharePluginStart;
|
||||
}
|
||||
|
||||
interface SetupDeps {
|
||||
share: SharePluginSetup;
|
||||
}
|
||||
|
||||
const APP_ID = 'urlGeneratorsExamples';
|
||||
|
||||
export class AccessLinksExamplesPlugin implements Plugin<void, void, SetupDeps, StartDeps> {
|
||||
public setup(core: CoreSetup<StartDeps>, { share: { urlGenerators } }: SetupDeps) {
|
||||
urlGenerators.registerUrlGenerator(
|
||||
createHelloPageLinkGenerator(async () => ({
|
||||
appBasePath: (await core.getStartServices())[0].application.getUrlForApp(APP_ID),
|
||||
}))
|
||||
);
|
||||
|
||||
urlGenerators.registerUrlGenerator(helloPageLinkGeneratorV1);
|
||||
|
||||
core.application.register({
|
||||
id: APP_ID,
|
||||
title: 'Access links examples',
|
||||
async mount(params: AppMountParameters) {
|
||||
const { renderApp } = await import('./app');
|
||||
return renderApp(
|
||||
{
|
||||
appBasePath: params.appBasePath,
|
||||
},
|
||||
params
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public start() {}
|
||||
|
||||
public stop() {}
|
||||
}
|
78
examples/url_generators_examples/public/url_generator.ts
Normal file
78
examples/url_generators_examples/public/url_generator.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
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: '' } };
|
||||
},
|
||||
};
|
15
examples/url_generators_examples/tsconfig.json
Normal file
15
examples/url_generators_examples/tsconfig.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./target",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"index.ts",
|
||||
"public/**/*.ts",
|
||||
"public/**/*.tsx",
|
||||
"server/**/*.ts",
|
||||
"../../typings/**/*"
|
||||
],
|
||||
"exclude": []
|
||||
}
|
8
examples/url_generators_explorer/README.md
Normal file
8
examples/url_generators_explorer/README.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
## Access links explorer
|
||||
|
||||
This example app shows how to:
|
||||
- Generate links to other applications
|
||||
- Generate dynamic links, when the target application is not known
|
||||
- Handle backward compatibility of urls
|
||||
|
||||
To run this example, use the command `yarn start --run-examples`.
|
10
examples/url_generators_explorer/kibana.json
Normal file
10
examples/url_generators_explorer/kibana.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"id": "urlGeneratorsExplorer",
|
||||
"version": "0.0.1",
|
||||
"kibanaVersion": "kibana",
|
||||
"configPath": ["url_generators_explorer"],
|
||||
"server": false,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["share", "urlGeneratorsExamples"],
|
||||
"optionalPlugins": []
|
||||
}
|
17
examples/url_generators_explorer/package.json
Normal file
17
examples/url_generators_explorer/package.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "url_generators_explorer",
|
||||
"version": "1.0.0",
|
||||
"main": "target/examples/url_generators_explorer",
|
||||
"kibana": {
|
||||
"version": "kibana",
|
||||
"templateVersion": "1.0.0"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"kbn": "node ../../scripts/kbn.js",
|
||||
"build": "rm -rf './target' && tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "3.5.3"
|
||||
}
|
||||
}
|
170
examples/url_generators_explorer/public/app.tsx
Normal file
170
examples/url_generators_explorer/public/app.tsx
Normal file
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { EuiPage } from '@elastic/eui';
|
||||
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { EuiPageBody } from '@elastic/eui';
|
||||
import { EuiPageContent } from '@elastic/eui';
|
||||
import { EuiPageContentBody } from '@elastic/eui';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import { EuiFieldText } from '@elastic/eui';
|
||||
import { EuiPageHeader } from '@elastic/eui';
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import { AppMountParameters } from '../../../src/core/public';
|
||||
import { UrlGeneratorsService } from '../../../src/plugins/share/public';
|
||||
import {
|
||||
HELLO_URL_GENERATOR,
|
||||
HELLO_URL_GENERATOR_V1,
|
||||
} from '../../url_generators_examples/public/url_generator';
|
||||
|
||||
interface Props {
|
||||
getLinkGenerator: UrlGeneratorsService['getUrlGenerator'];
|
||||
}
|
||||
|
||||
interface MigratedLink {
|
||||
isDeprecated: boolean;
|
||||
linkText: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
const ActionsExplorer = ({ getLinkGenerator }: Props) => {
|
||||
const [migratedLinks, setMigratedLinks] = useState([] as MigratedLink[]);
|
||||
const [buildingLinks, setBuildingLinks] = useState(false);
|
||||
const [firstName, setFirstName] = useState('');
|
||||
const [lastName, setLastName] = useState('');
|
||||
/**
|
||||
* 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.
|
||||
* They use deprecated generator ids.
|
||||
*/
|
||||
const [persistedLinks, setPersistedLinks] = useState([
|
||||
{
|
||||
id: HELLO_URL_GENERATOR_V1,
|
||||
linkText: 'Say hello to Mary',
|
||||
state: {
|
||||
name: 'Mary',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HELLO_URL_GENERATOR,
|
||||
linkText: 'Say hello to George',
|
||||
state: {
|
||||
firstName: 'George',
|
||||
lastName: 'Washington',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setBuildingLinks(true);
|
||||
|
||||
const updateLinks = async () => {
|
||||
const updatedLinks = await Promise.all(
|
||||
persistedLinks.map(async savedLink => {
|
||||
const generator = getLinkGenerator(savedLink.id);
|
||||
const link = await generator.createUrl(savedLink.state);
|
||||
return {
|
||||
isDeprecated: generator.isDeprecated,
|
||||
linkText: savedLink.linkText,
|
||||
link,
|
||||
};
|
||||
})
|
||||
);
|
||||
setMigratedLinks(updatedLinks);
|
||||
setBuildingLinks(false);
|
||||
};
|
||||
|
||||
updateLinks();
|
||||
}, [getLinkGenerator, persistedLinks]);
|
||||
|
||||
return (
|
||||
<EuiPage>
|
||||
<EuiPageBody>
|
||||
<EuiPageHeader>Access links explorer</EuiPageHeader>
|
||||
<EuiPageContent>
|
||||
<EuiPageContentBody>
|
||||
<EuiText>
|
||||
<p>Create new links using the most recent version of a url generator.</p>
|
||||
</EuiText>
|
||||
<EuiFieldText
|
||||
prepend="First name"
|
||||
onChange={e => {
|
||||
setFirstName(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<EuiFieldText prepend="Last name" onChange={e => setLastName(e.target.value)} />
|
||||
<EuiButton
|
||||
onClick={() =>
|
||||
setPersistedLinks([
|
||||
...persistedLinks,
|
||||
{
|
||||
id: HELLO_URL_GENERATOR,
|
||||
state: { firstName, lastName },
|
||||
linkText: `Say hello to ${firstName} ${lastName}`,
|
||||
},
|
||||
])
|
||||
}
|
||||
>
|
||||
Add new link
|
||||
</EuiButton>
|
||||
<EuiSpacer />
|
||||
|
||||
<EuiText>
|
||||
<p>
|
||||
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
|
||||
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
|
||||
migration function.
|
||||
</p>
|
||||
</EuiText>
|
||||
{buildingLinks ? (
|
||||
<div>loading...</div>
|
||||
) : (
|
||||
migratedLinks.map(link => (
|
||||
<React.Fragment>
|
||||
<EuiLink
|
||||
color={link.isDeprecated ? 'danger' : 'primary'}
|
||||
data-test-subj="linkToHelloPage"
|
||||
href={link.link}
|
||||
target="_blank"
|
||||
>
|
||||
{link.linkText}
|
||||
</EuiLink>
|
||||
<br />
|
||||
</React.Fragment>
|
||||
))
|
||||
)}
|
||||
</EuiPageContentBody>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
);
|
||||
};
|
||||
|
||||
export const renderApp = (props: Props, { element }: AppMountParameters) => {
|
||||
ReactDOM.render(<ActionsExplorer {...props} />, element);
|
||||
|
||||
return () => ReactDOM.unmountComponentAtNode(element);
|
||||
};
|
22
examples/url_generators_explorer/public/index.ts
Normal file
22
examples/url_generators_explorer/public/index.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { AccessLinksExplorerPlugin } from './plugin';
|
||||
|
||||
export const plugin = () => new AccessLinksExplorerPlugin();
|
51
examples/url_generators_explorer/public/page.tsx
Normal file
51
examples/url_generators_explorer/public/page.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
EuiPageContentBody,
|
||||
EuiPageHeader,
|
||||
EuiPageHeaderSection,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
interface PageProps {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Page({ title, children }: PageProps) {
|
||||
return (
|
||||
<EuiPageBody data-test-subj="searchTestPage">
|
||||
<EuiPageHeader>
|
||||
<EuiPageHeaderSection>
|
||||
<EuiTitle size="l">
|
||||
<h1>{title}</h1>
|
||||
</EuiTitle>
|
||||
</EuiPageHeaderSection>
|
||||
</EuiPageHeader>
|
||||
<EuiPageContent>
|
||||
<EuiPageContentBody>{children}</EuiPageContentBody>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
);
|
||||
}
|
48
examples/url_generators_explorer/public/plugin.tsx
Normal file
48
examples/url_generators_explorer/public/plugin.tsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { SharePluginStart } from 'src/plugins/share/public';
|
||||
import { Plugin, CoreSetup, AppMountParameters } from '../../../src/core/public';
|
||||
|
||||
interface StartDeps {
|
||||
share: SharePluginStart;
|
||||
}
|
||||
|
||||
export class AccessLinksExplorerPlugin implements Plugin<void, void, {}, StartDeps> {
|
||||
public setup(core: CoreSetup<StartDeps>) {
|
||||
core.application.register({
|
||||
id: 'urlGeneratorsExplorer',
|
||||
title: 'Access links explorer',
|
||||
async mount(params: AppMountParameters) {
|
||||
const depsStart = (await core.getStartServices())[1];
|
||||
const { renderApp } = await import('./app');
|
||||
return renderApp(
|
||||
{
|
||||
getLinkGenerator: depsStart.share.urlGenerators.getUrlGenerator,
|
||||
},
|
||||
params
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public start() {}
|
||||
|
||||
public stop() {}
|
||||
}
|
15
examples/url_generators_explorer/tsconfig.json
Normal file
15
examples/url_generators_explorer/tsconfig.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./target",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"index.ts",
|
||||
"public/**/*.ts",
|
||||
"public/**/*.tsx",
|
||||
"server/**/*.ts",
|
||||
"../../typings/**/*"
|
||||
],
|
||||
"exclude": []
|
||||
}
|
|
@ -2,10 +2,14 @@
|
|||
"id": "dashboard_embeddable_container",
|
||||
"version": "kibana",
|
||||
"requiredPlugins": [
|
||||
"data",
|
||||
"embeddable",
|
||||
"inspector",
|
||||
"uiActions"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"share"
|
||||
],
|
||||
"server": false,
|
||||
"ui": true
|
||||
}
|
||||
|
|
|
@ -31,3 +31,5 @@ export function plugin(initializerContext: PluginInitializerContext) {
|
|||
}
|
||||
|
||||
export { DashboardEmbeddableContainerPublicPlugin as Plugin };
|
||||
|
||||
export { DASHBOARD_APP_URL_GENERATOR } from './url_generator';
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public';
|
||||
import { SharePluginSetup } from 'src/plugins/share/public';
|
||||
import { UiActionsSetup, UiActionsStart } from '../../../plugins/ui_actions/public';
|
||||
import { CONTEXT_MENU_TRIGGER, IEmbeddableSetup, IEmbeddableStart } from './embeddable_plugin';
|
||||
import { ExpandPanelAction, ReplacePanelAction } from '.';
|
||||
|
@ -33,10 +34,22 @@ import {
|
|||
} from '../../../plugins/kibana_react/public';
|
||||
import { ExpandPanelActionContext, ACTION_EXPAND_PANEL } from './actions/expand_panel_action';
|
||||
import { ReplacePanelActionContext, ACTION_REPLACE_PANEL } from './actions/replace_panel_action';
|
||||
import {
|
||||
DashboardAppLinkGeneratorState,
|
||||
DASHBOARD_APP_URL_GENERATOR,
|
||||
createDirectAccessDashboardLinkGenerator,
|
||||
} from './url_generator';
|
||||
|
||||
declare module '../../share/public' {
|
||||
export interface UrlGeneratorStateMapping {
|
||||
[DASHBOARD_APP_URL_GENERATOR]: DashboardAppLinkGeneratorState;
|
||||
}
|
||||
}
|
||||
|
||||
interface SetupDependencies {
|
||||
embeddable: IEmbeddableSetup;
|
||||
uiActions: UiActionsSetup;
|
||||
share?: SharePluginSetup;
|
||||
}
|
||||
|
||||
interface StartDependencies {
|
||||
|
@ -59,10 +72,20 @@ export class DashboardEmbeddableContainerPublicPlugin
|
|||
implements Plugin<Setup, Start, SetupDependencies, StartDependencies> {
|
||||
constructor(initializerContext: PluginInitializerContext) {}
|
||||
|
||||
public setup(core: CoreSetup, { embeddable, uiActions }: SetupDependencies): Setup {
|
||||
public setup(core: CoreSetup, { share, uiActions }: SetupDependencies): Setup {
|
||||
const expandPanelAction = new ExpandPanelAction();
|
||||
uiActions.registerAction(expandPanelAction);
|
||||
uiActions.attachAction(CONTEXT_MENU_TRIGGER, expandPanelAction);
|
||||
const startServices = core.getStartServices();
|
||||
|
||||
if (share) {
|
||||
share.urlGenerators.registerUrlGenerator(
|
||||
createDirectAccessDashboardLinkGenerator(async () => ({
|
||||
appBasePath: (await startServices)[0].application.getUrlForApp('dashboard'),
|
||||
useHashedUrl: (await startServices)[0].uiSettings.get('state:storeInSessionStorage'),
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public start(core: CoreStart, plugins: StartDependencies): Start {
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { createDirectAccessDashboardLinkGenerator } from './url_generator';
|
||||
import { hashedItemStore } from '../../kibana_utils/public';
|
||||
// eslint-disable-next-line
|
||||
import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock';
|
||||
|
||||
const APP_BASE_PATH: string = 'xyz/app/kibana';
|
||||
|
||||
describe('dashboard url generator', () => {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
hashedItemStore.storage = mockStorage;
|
||||
});
|
||||
|
||||
test('creates a link to a saved dashboard', async () => {
|
||||
const generator = createDirectAccessDashboardLinkGenerator(() =>
|
||||
Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false })
|
||||
);
|
||||
const url = await generator.createUrl!({});
|
||||
expect(url).toMatchInlineSnapshot(`"xyz/app/kibana#/dashboard?_a=()&_g=()"`);
|
||||
});
|
||||
|
||||
test('creates a link with global time range set up', async () => {
|
||||
const generator = createDirectAccessDashboardLinkGenerator(() =>
|
||||
Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false })
|
||||
);
|
||||
const url = await generator.createUrl!({
|
||||
timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
|
||||
});
|
||||
expect(url).toMatchInlineSnapshot(
|
||||
`"xyz/app/kibana#/dashboard?_a=()&_g=(time:(from:now-15m,mode:relative,to:now))"`
|
||||
);
|
||||
});
|
||||
|
||||
test('creates a link with filters, time range and query to a saved object', async () => {
|
||||
const generator = createDirectAccessDashboardLinkGenerator(() =>
|
||||
Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false })
|
||||
);
|
||||
const url = await generator.createUrl!({
|
||||
timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
|
||||
dashboardId: '123',
|
||||
filters: [
|
||||
{
|
||||
meta: {
|
||||
alias: null,
|
||||
disabled: false,
|
||||
negate: false,
|
||||
},
|
||||
query: { query: 'hi' },
|
||||
},
|
||||
],
|
||||
query: { query: 'bye', language: 'kuery' },
|
||||
});
|
||||
expect(url).toMatchInlineSnapshot(
|
||||
`"xyz/app/kibana#/dashboard/123?_a=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),query:(language:kuery,query:bye))&_g=(time:(from:now-15m,mode:relative,to:now))"`
|
||||
);
|
||||
});
|
||||
|
||||
test('if no useHash setting is given, uses the one was start services', async () => {
|
||||
const generator = createDirectAccessDashboardLinkGenerator(() =>
|
||||
Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: true })
|
||||
);
|
||||
const url = await generator.createUrl!({
|
||||
timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
|
||||
});
|
||||
expect(url.indexOf('relative')).toBe(-1);
|
||||
});
|
||||
|
||||
test('can override a false useHash ui setting', async () => {
|
||||
const generator = createDirectAccessDashboardLinkGenerator(() =>
|
||||
Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false })
|
||||
);
|
||||
const url = await generator.createUrl!({
|
||||
timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
|
||||
useHash: true,
|
||||
});
|
||||
expect(url.indexOf('relative')).toBe(-1);
|
||||
});
|
||||
|
||||
test('can override a true useHash ui setting', async () => {
|
||||
const generator = createDirectAccessDashboardLinkGenerator(() =>
|
||||
Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: true })
|
||||
);
|
||||
const url = await generator.createUrl!({
|
||||
timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
|
||||
useHash: false,
|
||||
});
|
||||
expect(url.indexOf('relative')).toBeGreaterThan(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { TimeRange, Filter, Query } from '../../data/public';
|
||||
import { setStateToKbnUrl } from '../../kibana_utils/public';
|
||||
import { UrlGeneratorsDefinition, UrlGeneratorState } from '../../share/public';
|
||||
|
||||
export const STATE_STORAGE_KEY = '_a';
|
||||
export const GLOBAL_STATE_STORAGE_KEY = '_g';
|
||||
|
||||
export const DASHBOARD_APP_URL_GENERATOR = 'DASHBOARD_APP_URL_GENERATOR';
|
||||
|
||||
export type DashboardAppLinkGeneratorState = UrlGeneratorState<{
|
||||
/**
|
||||
* If given, the dashboard saved object with this id will be loaded. If not given,
|
||||
* a new, unsaved dashboard will be loaded up.
|
||||
*/
|
||||
dashboardId?: string;
|
||||
/**
|
||||
* Optionally set the time range in the time picker.
|
||||
*/
|
||||
timeRange?: TimeRange;
|
||||
/**
|
||||
* Optionally apply filers. NOTE: if given and used in conjunction with `dashboardId`, and the
|
||||
* saved dashboard has filters saved with it, this will _replace_ those filters. This will set
|
||||
* app filters, not global filters.
|
||||
*/
|
||||
filters?: Filter[];
|
||||
/**
|
||||
* Optionally set a query. NOTE: if given and used in conjunction with `dashboardId`, and the
|
||||
* saved dashboard has a query saved with it, this will _replace_ that query.
|
||||
*/
|
||||
query?: Query;
|
||||
/**
|
||||
* If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines
|
||||
* whether to hash the data in the url to avoid url length issues.
|
||||
*/
|
||||
useHash?: boolean;
|
||||
}>;
|
||||
|
||||
export const createDirectAccessDashboardLinkGenerator = (
|
||||
getStartServices: () => Promise<{ appBasePath: string; useHashedUrl: boolean }>
|
||||
): UrlGeneratorsDefinition<typeof DASHBOARD_APP_URL_GENERATOR> => ({
|
||||
id: DASHBOARD_APP_URL_GENERATOR,
|
||||
createUrl: async state => {
|
||||
const startServices = await getStartServices();
|
||||
const useHash = state.useHash ?? startServices.useHashedUrl;
|
||||
const appBasePath = startServices.appBasePath;
|
||||
const hash = state.dashboardId ? `dashboard/${state.dashboardId}` : `dashboard`;
|
||||
|
||||
const appStateUrl = setStateToKbnUrl(
|
||||
STATE_STORAGE_KEY,
|
||||
{
|
||||
query: state.query,
|
||||
filters: state.filters,
|
||||
},
|
||||
{ useHash },
|
||||
`${appBasePath}#/${hash}`
|
||||
);
|
||||
|
||||
return setStateToKbnUrl(
|
||||
GLOBAL_STATE_STORAGE_KEY,
|
||||
{
|
||||
time: state.timeRange,
|
||||
},
|
||||
{ useHash },
|
||||
appStateUrl
|
||||
);
|
||||
},
|
||||
});
|
|
@ -25,4 +25,5 @@ export interface RefreshInterval {
|
|||
export interface TimeRange {
|
||||
from: string;
|
||||
to: string;
|
||||
mode?: 'absolute' | 'relative';
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { UrlGeneratorStateMapping } from './url_generators/url_generator_definition';
|
||||
|
||||
export { SharePluginSetup, SharePluginStart } from './plugin';
|
||||
export {
|
||||
ShareContext,
|
||||
|
@ -25,6 +27,15 @@ export {
|
|||
ShowShareMenuOptions,
|
||||
ShareContextMenuPanelItem,
|
||||
} from './types';
|
||||
|
||||
export {
|
||||
UrlGeneratorId,
|
||||
UrlGeneratorState,
|
||||
UrlGeneratorsDefinition,
|
||||
UrlGeneratorContract,
|
||||
UrlGeneratorsService,
|
||||
} from './url_generators';
|
||||
|
||||
import { SharePlugin } from './plugin';
|
||||
|
||||
export const plugin = () => new SharePlugin();
|
||||
|
|
|
@ -21,27 +21,39 @@ import { CoreSetup, CoreStart, Plugin } from 'src/core/public';
|
|||
import { ShareMenuManager, ShareMenuManagerStart } from './services';
|
||||
import { ShareMenuRegistry, ShareMenuRegistrySetup } from './services';
|
||||
import { createShortUrlRedirectApp } from './services/short_url_redirect_app';
|
||||
import {
|
||||
UrlGeneratorsService,
|
||||
UrlGeneratorsSetup,
|
||||
UrlGeneratorsStart,
|
||||
} from './url_generators/url_generator_service';
|
||||
|
||||
export class SharePlugin implements Plugin<SharePluginSetup, SharePluginStart> {
|
||||
private readonly shareMenuRegistry = new ShareMenuRegistry();
|
||||
private readonly shareContextMenu = new ShareMenuManager();
|
||||
private readonly urlGeneratorsService = new UrlGeneratorsService();
|
||||
|
||||
public async setup(core: CoreSetup) {
|
||||
public setup(core: CoreSetup): SharePluginSetup {
|
||||
core.application.register(createShortUrlRedirectApp(core, window.location));
|
||||
return {
|
||||
...this.shareMenuRegistry.setup(),
|
||||
urlGenerators: this.urlGeneratorsService.setup(core),
|
||||
};
|
||||
}
|
||||
|
||||
public async start(core: CoreStart) {
|
||||
public start(core: CoreStart): SharePluginStart {
|
||||
return {
|
||||
...this.shareContextMenu.start(core, this.shareMenuRegistry.start()),
|
||||
urlGenerators: this.urlGeneratorsService.start(core),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type SharePluginSetup = ShareMenuRegistrySetup;
|
||||
export type SharePluginSetup = ShareMenuRegistrySetup & {
|
||||
urlGenerators: UrlGeneratorsSetup;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type SharePluginStart = ShareMenuManagerStart;
|
||||
export type SharePluginStart = ShareMenuManagerStart & {
|
||||
urlGenerators: UrlGeneratorsStart;
|
||||
};
|
||||
|
|
114
src/plugins/share/public/url_generators/README.md
Normal file
114
src/plugins/share/public/url_generators/README.md
Normal file
|
@ -0,0 +1,114 @@
|
|||
## URL Generator Services
|
||||
|
||||
Developers who maintain pages in Kibana that other developers may want to link to
|
||||
can register a direct access link generator. This provides 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 Arch team.
|
||||
|
||||
## Producer Usage
|
||||
|
||||
If you are registering a new generator, don't forget to add a mapping of id to state
|
||||
|
||||
```ts
|
||||
declare module '../../share/public' {
|
||||
export interface UrlGeneratorStateMapping {
|
||||
[MY_GENERATOR]: MyState;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Migration
|
||||
|
||||
Once your generator is released, you should *never* change the `MyState` type, nor the value of `MY_GENERATOR`.
|
||||
Instead, register a new generator id, with the new state type, and add a migration function to convert to it.
|
||||
|
||||
To avoid having to refactor many run time usages of the old id, change the _value_ of the generator id, but not
|
||||
the name itself. For example:
|
||||
|
||||
Initial release:
|
||||
```ts
|
||||
export const MY_GENERATOR = 'MY_GENERATOR';
|
||||
export const MyState {
|
||||
foo: string;
|
||||
}
|
||||
export interface UrlGeneratorStateMapping {
|
||||
[MY_GENERATOR]: UrlGeneratorState<MyState>;
|
||||
}
|
||||
```
|
||||
|
||||
Second release:
|
||||
```ts
|
||||
// Value stays the same here! This is important.
|
||||
export const MY_LEGACY_GENERATOR_V1 = 'MY_GENERATOR';
|
||||
// Always point the const `MY_GENERATOR` to the most
|
||||
// recent version of the state to avoid a large refactor.
|
||||
export const MY_GENERATOR = 'MY_GENERATOR_V2';
|
||||
|
||||
// Same here, the mapping stays the same, but the names change.
|
||||
export const MyLegacyState {
|
||||
foo: string;
|
||||
}
|
||||
// New type, old name!
|
||||
export const MyState {
|
||||
bar: string;
|
||||
}
|
||||
export interface UrlGeneratorStateMapping {
|
||||
[MY_LEGACY_GENERATOR_V1]: UrlGeneratorState<MyLegacyState, typeof MY_GENERATOR, MyState>;
|
||||
[MY_GENERATOR]: UrlGeneratorState<MyState>;
|
||||
}
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
Working examples of registered link generators can be found in `examples/url_generator_examples` folder. Run these
|
||||
examples via
|
||||
|
||||
```
|
||||
yarn start --run-examples
|
||||
```
|
||||
|
||||
## Consumer Usage
|
||||
|
||||
Consumers of this service can use the ids and state to create URL strings:
|
||||
|
||||
```ts
|
||||
const { id, state } = getLinkData();
|
||||
const generator = urlGeneratorPluginStart.getLinkGenerator(id);
|
||||
if (generator.isDeprecated) {
|
||||
// Consumers have a few options here.
|
||||
|
||||
// If the consumer constrols the persisted data, they can migrate this data and
|
||||
// update it. Something like this:
|
||||
const { id: newId, state: newState } = await generator.migrate(state);
|
||||
replaceLegacyData({ oldId: id, newId, newState });
|
||||
|
||||
// If the consumer does not control the persisted data store, they can warn the
|
||||
// user that they are using a deprecated id and should update the data on their
|
||||
// own.
|
||||
alert(`This data is deprecated, please generate new URL data.`);
|
||||
|
||||
// They can also choose to do nothing. Calling `createUrl` will internally migrate this
|
||||
// data. Depending on the cost, we may choose to keep support for deprecated generators
|
||||
// along for a long time, using telemetry to make this decision. However another
|
||||
// consideration is how many migrations are taking place and whether this is creating a
|
||||
// performance issue.
|
||||
}
|
||||
const link = await generator.createUrl(savedLink.state);
|
||||
```
|
||||
|
||||
**As a consumer, you should not persist the url string!**
|
||||
|
||||
As soon as you do, you have lost your migration options. Instead you should store the id
|
||||
and the state object. This will let you recreate the migrated url later.
|
||||
|
||||
### Examples
|
||||
|
||||
Working examples of consuming registered link generators can be found in `examples/url_generator_explorer` folder. Run these
|
||||
via
|
||||
|
||||
```
|
||||
yarn start --run-examples
|
||||
```
|
24
src/plugins/share/public/url_generators/index.ts
Normal file
24
src/plugins/share/public/url_generators/index.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export * from './url_generator_service';
|
||||
|
||||
export * from './url_generator_definition';
|
||||
|
||||
export * from './url_generator_contract';
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { UrlGeneratorId, UrlGeneratorStateMapping } from './url_generator_definition';
|
||||
|
||||
export interface UrlGeneratorContract<Id extends UrlGeneratorId> {
|
||||
id: Id;
|
||||
createUrl(state: UrlGeneratorStateMapping[Id]['State']): Promise<string>;
|
||||
isDeprecated: boolean;
|
||||
migrate(
|
||||
state: UrlGeneratorStateMapping[Id]['State']
|
||||
): Promise<{
|
||||
state: UrlGeneratorStateMapping[Id]['MigratedState'];
|
||||
id: UrlGeneratorStateMapping[Id]['MigratedId'];
|
||||
}>;
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export type UrlGeneratorId = string;
|
||||
|
||||
export interface UrlGeneratorState<
|
||||
S extends {},
|
||||
I extends string | undefined = undefined,
|
||||
MS extends {} | undefined = undefined
|
||||
> {
|
||||
State: S;
|
||||
MigratedId?: I;
|
||||
MigratedState?: MS;
|
||||
}
|
||||
|
||||
export interface UrlGeneratorStateMapping {
|
||||
// The `any` here is quite unfortunate. Using `object` actually gives no type errors in my IDE
|
||||
// but running `node scripts/type_check` will cause an error:
|
||||
// examples/url_generators_examples/public/url_generator.ts:77:66 -
|
||||
// error TS2339: Property 'name' does not exist on type 'object'. However it's correctly
|
||||
// typed when I edit that file.
|
||||
[key: string]: UrlGeneratorState<any, string | undefined, object | undefined>;
|
||||
}
|
||||
|
||||
export interface UrlGeneratorsDefinition<Id extends UrlGeneratorId> {
|
||||
id: Id;
|
||||
createUrl?: (state: UrlGeneratorStateMapping[Id]['State']) => Promise<string>;
|
||||
isDeprecated?: boolean;
|
||||
migrate?: (
|
||||
state: UrlGeneratorStateMapping[Id]['State']
|
||||
) => Promise<{
|
||||
state: UrlGeneratorStateMapping[Id]['MigratedState'];
|
||||
id: UrlGeneratorStateMapping[Id]['MigratedId'];
|
||||
}>;
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { UrlGeneratorsStart } from './url_generator_service';
|
||||
import {
|
||||
UrlGeneratorStateMapping,
|
||||
UrlGeneratorId,
|
||||
UrlGeneratorsDefinition,
|
||||
} from './url_generator_definition';
|
||||
import { UrlGeneratorContract } from './url_generator_contract';
|
||||
|
||||
export class UrlGeneratorInternal<Id extends UrlGeneratorId> {
|
||||
constructor(
|
||||
private spec: UrlGeneratorsDefinition<Id>,
|
||||
private getGenerator: UrlGeneratorsStart['getUrlGenerator']
|
||||
) {
|
||||
if (spec.isDeprecated && !spec.migrate) {
|
||||
throw new Error(
|
||||
i18n.translate('share.urlGenerators.error.noMigrationFnProvided', {
|
||||
defaultMessage:
|
||||
'If the access link generator is marked as deprecated, you must provide a migration function.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (!spec.isDeprecated && spec.migrate) {
|
||||
throw new Error(
|
||||
i18n.translate('share.urlGenerators.error.migrationFnGivenNotDeprecated', {
|
||||
defaultMessage:
|
||||
'If you provide a migration function, you must mark this generator as deprecated',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (!spec.createUrl && !spec.isDeprecated) {
|
||||
throw new Error(
|
||||
i18n.translate('share.urlGenerators.error.noCreateUrlFnProvided', {
|
||||
defaultMessage:
|
||||
'This generator is not marked as deprecated. Please provide a createUrl fn.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (spec.createUrl && spec.isDeprecated) {
|
||||
throw new Error(
|
||||
i18n.translate('share.urlGenerators.error.createUrlFnProvided', {
|
||||
defaultMessage: 'This generator is marked as deprecated. Do not supply a createUrl fn.',
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getPublicContract(): UrlGeneratorContract<Id> {
|
||||
return {
|
||||
id: this.spec.id,
|
||||
createUrl: async (state: UrlGeneratorStateMapping[Id]['State']) => {
|
||||
if (this.spec.migrate && !this.spec.createUrl) {
|
||||
const { id, state: newState } = await this.spec.migrate(state);
|
||||
|
||||
// eslint-disable-next-line
|
||||
console.warn(`URL generator is deprecated and may not work in future versions. Please migrate your data.`);
|
||||
|
||||
return this.getGenerator(id!).createUrl(newState!);
|
||||
}
|
||||
|
||||
return this.spec.createUrl!(state);
|
||||
},
|
||||
isDeprecated: !!this.spec.isDeprecated,
|
||||
migrate: async (state: UrlGeneratorStateMapping[Id]['State']) => {
|
||||
if (!this.spec.isDeprecated) {
|
||||
throw new Error(
|
||||
i18n.translate('share.urlGenerators.error.migrateCalledNotDeprecated', {
|
||||
defaultMessage: 'You cannot call migrate on a non-deprecated generator.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return this.spec.migrate!(state);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { UrlGeneratorsService } from './url_generator_service';
|
||||
import { coreMock } from '../../../../core/public/mocks';
|
||||
|
||||
const service = new UrlGeneratorsService();
|
||||
|
||||
const setup = service.setup(coreMock.createSetup());
|
||||
const start = service.start(coreMock.createStart());
|
||||
|
||||
test('Asking for a generator that does not exist throws an error', () => {
|
||||
expect(() => start.getUrlGenerator('noexist')).toThrowError();
|
||||
});
|
||||
|
||||
test('Registering and retrieving a generator', async () => {
|
||||
setup.registerUrlGenerator({
|
||||
id: 'TEST_GENERATOR',
|
||||
createUrl: () => Promise.resolve('myurl'),
|
||||
});
|
||||
const generator = start.getUrlGenerator('TEST_GENERATOR');
|
||||
expect(generator).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"createUrl": [Function],
|
||||
"id": "TEST_GENERATOR",
|
||||
"isDeprecated": false,
|
||||
"migrate": [Function],
|
||||
}
|
||||
`);
|
||||
await expect(generator.migrate({})).rejects.toEqual(
|
||||
new Error('You cannot call migrate on a non-deprecated generator.')
|
||||
);
|
||||
expect(await generator.createUrl({})).toBe('myurl');
|
||||
});
|
||||
|
||||
test('Registering a generator with a createUrl function that is deprecated throws an error', () => {
|
||||
expect(() =>
|
||||
setup.registerUrlGenerator({
|
||||
id: 'TEST_GENERATOR',
|
||||
migrate: () => Promise.resolve({ id: '', state: {} }),
|
||||
createUrl: () => Promise.resolve('myurl'),
|
||||
isDeprecated: true,
|
||||
})
|
||||
).toThrowError(
|
||||
new Error('This generator is marked as deprecated. Do not supply a createUrl fn.')
|
||||
);
|
||||
});
|
||||
|
||||
test('Registering a deprecated generator with no migration function throws an error', () => {
|
||||
expect(() =>
|
||||
setup.registerUrlGenerator({
|
||||
id: 'TEST_GENERATOR',
|
||||
isDeprecated: true,
|
||||
})
|
||||
).toThrowError(
|
||||
new Error(
|
||||
'If the access link generator is marked as deprecated, you must provide a migration function.'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test('Registering a generator with no functions throws an error', () => {
|
||||
expect(() =>
|
||||
setup.registerUrlGenerator({
|
||||
id: 'TEST_GENERATOR',
|
||||
})
|
||||
).toThrowError(
|
||||
new Error('This generator is not marked as deprecated. Please provide a createUrl fn.')
|
||||
);
|
||||
});
|
||||
|
||||
test('Registering a generator with a migrate function that is not deprecated throws an error', () => {
|
||||
expect(() =>
|
||||
setup.registerUrlGenerator({
|
||||
id: 'TEST_GENERATOR',
|
||||
migrate: () => Promise.resolve({ id: '', state: {} }),
|
||||
isDeprecated: false,
|
||||
})
|
||||
).toThrowError(
|
||||
new Error('If you provide a migration function, you must mark this generator as deprecated')
|
||||
);
|
||||
});
|
||||
|
||||
test('Registering a generator with a migrate function and a createUrl fn throws an error', () => {
|
||||
expect(() =>
|
||||
setup.registerUrlGenerator({
|
||||
id: 'TEST_GENERATOR',
|
||||
createUrl: () => Promise.resolve('myurl'),
|
||||
migrate: () => Promise.resolve({ id: '', state: {} }),
|
||||
})
|
||||
).toThrowError();
|
||||
});
|
||||
|
||||
test('Generator returns migrated url', async () => {
|
||||
setup.registerUrlGenerator({
|
||||
id: 'v1',
|
||||
migrate: (state: { bar: string }) => Promise.resolve({ id: 'v2', state: { foo: state.bar } }),
|
||||
isDeprecated: true,
|
||||
});
|
||||
setup.registerUrlGenerator({
|
||||
id: 'v2',
|
||||
createUrl: (state: { foo: string }) => Promise.resolve(`www.${state.foo}.com`),
|
||||
isDeprecated: false,
|
||||
});
|
||||
|
||||
const generator = start.getUrlGenerator('v1');
|
||||
expect(generator.isDeprecated).toBe(true);
|
||||
expect(await generator.migrate({ bar: 'hi' })).toEqual({ id: 'v2', state: { foo: 'hi' } });
|
||||
expect(await generator.createUrl({ bar: 'hi' })).toEqual('www.hi.com');
|
||||
});
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { CoreSetup, CoreStart, Plugin } from 'src/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { UrlGeneratorId, UrlGeneratorsDefinition } from './url_generator_definition';
|
||||
import { UrlGeneratorInternal } from './url_generator_internal';
|
||||
import { UrlGeneratorContract } from './url_generator_contract';
|
||||
|
||||
export interface UrlGeneratorsStart {
|
||||
getUrlGenerator: (urlGeneratorId: UrlGeneratorId) => UrlGeneratorContract<UrlGeneratorId>;
|
||||
}
|
||||
|
||||
export interface UrlGeneratorsSetup {
|
||||
registerUrlGenerator: <Id extends UrlGeneratorId>(generator: UrlGeneratorsDefinition<Id>) => void;
|
||||
}
|
||||
|
||||
export class UrlGeneratorsService implements Plugin<UrlGeneratorsSetup, UrlGeneratorsStart> {
|
||||
// Unfortunate use of any here, but I haven't figured out how to type this any better without
|
||||
// getting warnings.
|
||||
private urlGenerators: Map<string, UrlGeneratorInternal<any>> = new Map();
|
||||
|
||||
constructor() {}
|
||||
|
||||
public setup(core: CoreSetup) {
|
||||
const setup: UrlGeneratorsSetup = {
|
||||
registerUrlGenerator: <Id extends UrlGeneratorId>(
|
||||
generatorOptions: UrlGeneratorsDefinition<Id>
|
||||
) => {
|
||||
this.urlGenerators.set(
|
||||
generatorOptions.id,
|
||||
new UrlGeneratorInternal<Id>(generatorOptions, this.getUrlGenerator)
|
||||
);
|
||||
},
|
||||
};
|
||||
return setup;
|
||||
}
|
||||
|
||||
public start(core: CoreStart) {
|
||||
const start: UrlGeneratorsStart = {
|
||||
getUrlGenerator: this.getUrlGenerator,
|
||||
};
|
||||
return start;
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
|
||||
private readonly getUrlGenerator = (id: UrlGeneratorId) => {
|
||||
const generator = this.urlGenerators.get(id);
|
||||
if (!generator) {
|
||||
throw new Error(
|
||||
i18n.translate('share.urlGenerators.errors.noGeneratorWithId', {
|
||||
defaultMessage: 'No generator found with id {id}',
|
||||
values: { id },
|
||||
})
|
||||
);
|
||||
}
|
||||
return generator.getPublicContract();
|
||||
};
|
||||
}
|
|
@ -7,6 +7,10 @@
|
|||
import { TIME_RANGE_TYPE, URL_TYPE } from './constants';
|
||||
|
||||
import rison from 'rison-node';
|
||||
import url from 'url';
|
||||
|
||||
import { npStart } from 'ui/new_platform';
|
||||
import { DASHBOARD_APP_URL_GENERATOR } from '../../../../../../../../../src/plugins/dashboard_embeddable_container/public';
|
||||
|
||||
import { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns';
|
||||
import { getPartitioningFieldNames } from '../../../../../common/util/job_utils';
|
||||
|
@ -153,52 +157,42 @@ function buildDashboardUrlFromSettings(settings) {
|
|||
query = searchSourceData.query;
|
||||
}
|
||||
|
||||
// Add time settings to the global state URL parameter with $earliest$ and
|
||||
// $latest$ tokens which get substituted for times around the time of the
|
||||
// anomaly on which the URL will be run against.
|
||||
const _g = rison.encode({
|
||||
time: {
|
||||
from: '$earliest$',
|
||||
to: '$latest$',
|
||||
mode: 'absolute',
|
||||
},
|
||||
});
|
||||
|
||||
const appState = {
|
||||
filters,
|
||||
};
|
||||
|
||||
// To put entities in filters section would involve creating parameters of the form
|
||||
// filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:b30fd340-efb4-11e7-a600-0f58b1422b87,
|
||||
// key:airline,negate:!f,params:(query:AAL,type:phrase),type:phrase,value:AAL),query:(match:(airline:(query:AAL,type:phrase)))))
|
||||
// which includes the ID of the index holding the field used in the filter.
|
||||
|
||||
// So for simplicity, put entities in the query, replacing any query which is there already.
|
||||
// e.g. query:(language:kuery,query:'region:us-east-1%20and%20instance:i-20d061fa')
|
||||
const queryFromEntityFieldNames = buildAppStateQueryParam(queryFieldNames);
|
||||
if (queryFromEntityFieldNames !== undefined) {
|
||||
query = queryFromEntityFieldNames;
|
||||
}
|
||||
|
||||
if (query !== undefined) {
|
||||
appState.query = query;
|
||||
}
|
||||
const generator = npStart.plugins.share.urlGenerators.getUrlGenerator(
|
||||
DASHBOARD_APP_URL_GENERATOR
|
||||
);
|
||||
|
||||
const _a = rison.encode(appState);
|
||||
return generator
|
||||
.createUrl({
|
||||
dashboardId,
|
||||
timeRange: {
|
||||
from: '$earliest$',
|
||||
to: '$latest$',
|
||||
mode: 'absolute',
|
||||
},
|
||||
filters,
|
||||
query,
|
||||
// Don't hash the URL since this string will be 1. shown to the user and 2. used as a
|
||||
// template to inject the time parameters.
|
||||
useHash: false,
|
||||
})
|
||||
.then(urlValue => {
|
||||
const urlToAdd = {
|
||||
url_name: settings.label,
|
||||
url_value: decodeURIComponent(`kibana${url.parse(urlValue).hash}`),
|
||||
time_range: TIME_RANGE_TYPE.AUTO,
|
||||
};
|
||||
|
||||
const urlValue = `kibana#/dashboard/${dashboardId}?_g=${_g}&_a=${_a}`;
|
||||
if (settings.timeRange.type === TIME_RANGE_TYPE.INTERVAL) {
|
||||
urlToAdd.time_range = settings.timeRange.interval;
|
||||
}
|
||||
|
||||
const urlToAdd = {
|
||||
url_name: settings.label,
|
||||
url_value: urlValue,
|
||||
time_range: TIME_RANGE_TYPE.AUTO,
|
||||
};
|
||||
|
||||
if (settings.timeRange.type === TIME_RANGE_TYPE.INTERVAL) {
|
||||
urlToAdd.time_range = settings.timeRange.interval;
|
||||
}
|
||||
|
||||
resolve(urlToAdd);
|
||||
resolve(urlToAdd);
|
||||
});
|
||||
})
|
||||
.catch(resp => {
|
||||
reject(resp);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue