Add direct access link registry and dashboard impl and use in ML (#57496) (#59433)

* 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:
Stacey Gammon 2020-03-05 12:28:48 -05:00 committed by GitHub
parent 0c12506aa7
commit 3538d5ac92
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 1461 additions and 43 deletions

View 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

View file

@ -0,0 +1,10 @@
{
"id": "urlGeneratorsExamples",
"version": "0.0.1",
"kibanaVersion": "kibana",
"configPath": ["url_generators_examples"],
"server": false,
"ui": true,
"requiredPlugins": ["share"],
"optionalPlugins": []
}

View 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"
}
}

View 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);
};

View 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();

View 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() {}
}

View 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: '' } };
},
};

View file

@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./target",
"skipLibCheck": true
},
"include": [
"index.ts",
"public/**/*.ts",
"public/**/*.tsx",
"server/**/*.ts",
"../../typings/**/*"
],
"exclude": []
}

View 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`.

View 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": []
}

View 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"
}
}

View 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);
};

View 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();

View 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>
);
}

View 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() {}
}

View file

@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./target",
"skipLibCheck": true
},
"include": [
"index.ts",
"public/**/*.ts",
"public/**/*.tsx",
"server/**/*.ts",
"../../typings/**/*"
],
"exclude": []
}

View file

@ -2,10 +2,14 @@
"id": "dashboard_embeddable_container",
"version": "kibana",
"requiredPlugins": [
"data",
"embeddable",
"inspector",
"uiActions"
],
"optionalPlugins": [
"share"
],
"server": false,
"ui": true
}

View file

@ -31,3 +31,5 @@ export function plugin(initializerContext: PluginInitializerContext) {
}
export { DashboardEmbeddableContainerPublicPlugin as Plugin };
export { DASHBOARD_APP_URL_GENERATOR } from './url_generator';

View file

@ -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 {

View file

@ -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);
});
});

View file

@ -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
);
},
});

View file

@ -25,4 +25,5 @@ export interface RefreshInterval {
export interface TimeRange {
from: string;
to: string;
mode?: 'absolute' | 'relative';
}

View file

@ -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();

View file

@ -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;
};

View 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
```

View 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';

View file

@ -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'];
}>;
}

View 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.
*/
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'];
}>;
}

View file

@ -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);
},
};
}
}

View file

@ -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');
});

View 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 { 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();
};
}

View file

@ -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);