mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Drilldown allow list (#85779)
* feat: 🎸 add ROW_CLICK_TRIGGER * feat: 🎸 wire row click event to UI Actions trigger in Lens * feat: 🎸 add row click trigger to url drilldown * feat: 🎸 add datatable to row click context * feat: 🎸 pass in row index in row click trigger context * feat: 🎸 add columns to row click trigger context * feat: 🎸 fill values and keys event scope array * feat: 🎸 generate correct row scope variables * fix: 🐛 report triggers from lens embeddable * feat: 🎸 add sample preview for row click trigger * feat: 🎸 remove url drilldown preview box * chore: 🤖 remove mock variable generation functions * feat: 🎸 generate context and global variable lists * feat: 🎸 preview event variable list * feat: 🎸 show empty url error on blur * feat: 🎸 add ability to always show popup for executed actions * refactor: 💡 rename multiple action execution method * fix: 🐛 don't add separator befor group on no main items * feat: 🎸 wire in uiActions service into datatable renderer * feat: 🎸 check each row if it has compatible row click actions * feat: 🎸 allow passing data to expression renderer * feat: 🎸 add isEmbeddable helper * feat: 🎸 pass embeddable to lens table renderer * feat: 🎸 hide lens table row actions which are empty * feat: 🎸 re-render lens embeddable when dynamic actions chagne * feat: 🎸 hide actions column if there are no row actions * feat: 🎸 re-render lens embeddable on view mode chagne * fix: 🐛 fix TypeScript errors * chore: 🤖 fix TypeScript errors * docs: ✏️ update auto-generated docs * feat: 🎸 add hasCompatibleActions to expression layer * feat: 🎸 remove "data" from expression renderer handlers * fix: 🐛 fix TypeScript errors * test: 💍 fix Jest tests * docs: ✏️ update autogenerated docs * fix: 🐛 wrap event payload into data * test: 💍 add "alwaysShowPopup" test * chore: 🤖 add comment requested in review https://github.com/elastic/kibana/pull/83167#discussion_r537340216 * test: 💍 add hasCompatibleActions test * test: 💍 add datatable renderer test * test: 💍 add Lens embeddable input change tests * test: 💍 add embeddable row click test * fix: 🐛 add url validation * test: 💍 add url drilldown tests * docs: ✏️ remove url drilldown preview from docs * docs: ✏️ remove preview from url templating * docs: ✏️ add row click description * chore: 🤖 move 36.5 KB bundle balance to url_drilldown * test: 💍 simplify test case * feat: 🎸 check if external URL is valid before redirecting user * test: 💍 check for external URL validity * feat: 🎸 check if external URL is allowed in exec and getHref * test: 💍 fix test import
This commit is contained in:
parent
edb3bb732e
commit
a57cba4978
3 changed files with 106 additions and 8 deletions
|
@ -4,9 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { IExternalUrl } from 'src/core/public';
|
||||
import { UrlDrilldown, ActionContext, Config } from './url_drilldown';
|
||||
import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public/lib/embeddables';
|
||||
import { DatatableColumnType } from '../../../../../../src/plugins/expressions/common';
|
||||
import { of } from '../../../../../../src/plugins/kibana_utils';
|
||||
import { createPoint, rowClickData, TestEmbeddable } from './test/data';
|
||||
import {
|
||||
VALUE_CLICK_TRIGGER,
|
||||
|
@ -59,13 +61,27 @@ const mockEmbeddable = ({
|
|||
|
||||
const mockNavigateToUrl = jest.fn(() => Promise.resolve());
|
||||
|
||||
describe('UrlDrilldown', () => {
|
||||
const urlDrilldown = new UrlDrilldown({
|
||||
class TextExternalUrl implements IExternalUrl {
|
||||
constructor(private readonly isCorrect: boolean = true) {}
|
||||
|
||||
public validateUrl(url: string): URL | null {
|
||||
return this.isCorrect ? new URL(url) : null;
|
||||
}
|
||||
}
|
||||
|
||||
const createDrilldown = (isExternalUrlValid: boolean = true) => {
|
||||
const drilldown = new UrlDrilldown({
|
||||
externalUrl: new TextExternalUrl(isExternalUrlValid),
|
||||
getGlobalScope: () => ({ kibanaUrl: 'http://localhost:5601/' }),
|
||||
getSyntaxHelpDocsLink: () => 'http://localhost:5601/docs',
|
||||
getVariablesHelpDocsLink: () => 'http://localhost:5601/docs',
|
||||
navigateToUrl: mockNavigateToUrl,
|
||||
});
|
||||
return drilldown;
|
||||
};
|
||||
|
||||
describe('UrlDrilldown', () => {
|
||||
const urlDrilldown = createDrilldown();
|
||||
|
||||
test('license', () => {
|
||||
expect(urlDrilldown.minimalLicense).toBe('gold');
|
||||
|
@ -125,6 +141,30 @@ describe('UrlDrilldown', () => {
|
|||
|
||||
await expect(urlDrilldown.isCompatible(config, context)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
test('not compatible if external URL is denied', async () => {
|
||||
const drilldown1 = createDrilldown(true);
|
||||
const drilldown2 = createDrilldown(false);
|
||||
const config: Config = {
|
||||
url: {
|
||||
template: `https://elasti.co/?{{event.value}}&{{rison context.panel.query}}`,
|
||||
},
|
||||
openInNewTab: false,
|
||||
};
|
||||
|
||||
const context: ActionContext = {
|
||||
data: {
|
||||
data: mockDataPoints,
|
||||
},
|
||||
embeddable: mockEmbeddable,
|
||||
};
|
||||
|
||||
const result1 = await drilldown1.isCompatible(config, context);
|
||||
const result2 = await drilldown2.isCompatible(config, context);
|
||||
|
||||
expect(result1).toBe(true);
|
||||
expect(result2).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHref & execute', () => {
|
||||
|
@ -173,6 +213,42 @@ describe('UrlDrilldown', () => {
|
|||
await expect(urlDrilldown.execute(config, context)).rejects.toThrowError();
|
||||
expect(mockNavigateToUrl).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('should throw on denied external URL', async () => {
|
||||
const drilldown1 = createDrilldown(true);
|
||||
const drilldown2 = createDrilldown(false);
|
||||
const config: Config = {
|
||||
url: {
|
||||
template: `https://elasti.co/?{{event.value}}&{{rison context.panel.query}}`,
|
||||
},
|
||||
openInNewTab: false,
|
||||
};
|
||||
|
||||
const context: ActionContext = {
|
||||
data: {
|
||||
data: mockDataPoints,
|
||||
},
|
||||
embeddable: mockEmbeddable,
|
||||
};
|
||||
|
||||
const url = await drilldown1.getHref(config, context);
|
||||
await drilldown1.execute(config, context);
|
||||
|
||||
expect(url).toMatchInlineSnapshot(`"https://elasti.co/?test&(language:kuery,query:test)"`);
|
||||
expect(mockNavigateToUrl).toBeCalledWith(url);
|
||||
|
||||
const [, error1] = await of(drilldown2.getHref(config, context));
|
||||
const [, error2] = await of(drilldown2.execute(config, context));
|
||||
|
||||
expect(error1).toBeInstanceOf(Error);
|
||||
expect(error1.message).toMatchInlineSnapshot(
|
||||
`"External URL [https://elasti.co/?test&(language:kuery,query:test)] was denied by ExternalUrl service. You can configure external URL policies using \\"externalUrl.policy\\" setting in kibana.yml."`
|
||||
);
|
||||
expect(error2).toBeInstanceOf(Error);
|
||||
expect(error2.message).toMatchInlineSnapshot(
|
||||
`"External URL [https://elasti.co/?test&(language:kuery,query:test)] was denied by ExternalUrl service. You can configure external URL policies using \\"externalUrl.policy\\" setting in kibana.yml."`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('variables', () => {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { getFlattenedObject } from '@kbn/std';
|
||||
import { IExternalUrl } from 'src/core/public';
|
||||
import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public';
|
||||
import {
|
||||
ChartActionContext,
|
||||
|
@ -31,6 +32,7 @@ import { getPanelVariables, getEventScope, getEventVariableList } from './url_dr
|
|||
import { txtUrlDrilldownDisplayName } from './i18n';
|
||||
|
||||
interface UrlDrilldownDeps {
|
||||
externalUrl: IExternalUrl;
|
||||
getGlobalScope: () => UrlDrilldownGlobalScope;
|
||||
navigateToUrl: (url: string) => Promise<void>;
|
||||
getSyntaxHelpDocsLink: () => string;
|
||||
|
@ -55,7 +57,7 @@ const URL_DRILLDOWN = 'URL_DRILLDOWN';
|
|||
export class UrlDrilldown implements Drilldown<Config, UrlTrigger, ActionFactoryContext> {
|
||||
public readonly id = URL_DRILLDOWN;
|
||||
|
||||
constructor(private deps: UrlDrilldownDeps) {}
|
||||
constructor(private readonly deps: UrlDrilldownDeps) {}
|
||||
|
||||
public readonly order = 8;
|
||||
|
||||
|
@ -109,18 +111,37 @@ export class UrlDrilldown implements Drilldown<Config, UrlTrigger, ActionFactory
|
|||
console.warn(
|
||||
`UrlDrilldown [${config.url.template}] is not valid. Error [${error}]. Skipping execution.`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return Promise.resolve(isValid);
|
||||
const url = this.buildUrl(config, context);
|
||||
const validUrl = this.deps.externalUrl.validateUrl(url);
|
||||
if (!validUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
public readonly getHref = async (config: Config, context: ActionContext) => {
|
||||
const scope = this.getRuntimeVariables(context);
|
||||
return urlDrilldownCompileUrl(config.url.template, scope);
|
||||
private buildUrl(config: Config, context: ActionContext): string {
|
||||
const url = urlDrilldownCompileUrl(config.url.template, this.getRuntimeVariables(context));
|
||||
return url;
|
||||
}
|
||||
|
||||
public readonly getHref = async (config: Config, context: ActionContext): Promise<string> => {
|
||||
const url = this.buildUrl(config, context);
|
||||
const validUrl = this.deps.externalUrl.validateUrl(url);
|
||||
if (!validUrl) {
|
||||
throw new Error(
|
||||
`External URL [${url}] was denied by ExternalUrl service. ` +
|
||||
`You can configure external URL policies using "externalUrl.policy" setting in kibana.yml.`
|
||||
);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
public readonly execute = async (config: Config, context: ActionContext) => {
|
||||
const url = urlDrilldownCompileUrl(config.url.template, this.getRuntimeVariables(context));
|
||||
const url = await this.getHref(config, context);
|
||||
if (config.openInNewTab) {
|
||||
window.open(url, '_blank', 'noopener');
|
||||
} else {
|
||||
|
|
|
@ -38,6 +38,7 @@ export class UrlDrilldownPlugin
|
|||
const startServices = createStartServicesGetter(core.getStartServices);
|
||||
plugins.uiActionsEnhanced.registerDrilldown(
|
||||
new UrlDrilldown({
|
||||
externalUrl: core.http.externalUrl,
|
||||
getGlobalScope: urlDrilldownGlobalScopeProvider({ core }),
|
||||
navigateToUrl: (url: string) =>
|
||||
core.getStartServices().then(([{ application }]) => application.navigateToUrl(url)),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue