Skip to content

Commit 98485a5

Browse files
[7.x] URL encoding for URL drilldown (#86902) (#87143)
* URL encoding for URL drilldown (#86902) * feat: 🎸 use EuiSwitch for "Open in new window" toggle * feat: 🎸 add "URL encoding" option and "Additional options" * feat: 🎸 make "Open in new window" true by default * feat: 🎸 respect encoding config setting * test: 💍 add encoding tests * feat: 🎸 add URI encoding Handlebars helpers * docs: ✏️ add URL encoding methods to URL Drilldown docs Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * test: 💍 align 7.x branch with master Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
1 parent 3e50b5f commit 98485a5

File tree

9 files changed

+185
-18
lines changed

9 files changed

+185
-18
lines changed

docs/user/dashboard/url-drilldown.asciidoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ Example:
133133

134134
`{{split event.value ","}}`
135135

136+
|encodeURIComponent
137+
a|Escapes string using built in `encodeURIComponent` function.
138+
139+
|encodeURIQuery
140+
a|Escapes string using built in `encodeURIComponent` function, while keeping "@", ":", "$", ",", and ";" characters as is.
141+
136142
|===
137143

138144

x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,3 +443,77 @@ describe('UrlDrilldown', () => {
443443
});
444444
});
445445
});
446+
447+
describe('encoding', () => {
448+
const urlDrilldown = createDrilldown();
449+
const context: ActionContext = {
450+
data: {
451+
data: mockDataPoints,
452+
},
453+
embeddable: mockEmbeddable,
454+
};
455+
456+
test('encodes URL by default', async () => {
457+
const config: Config = {
458+
url: {
459+
template: 'https://elastic.co?foo=head%26shoulders',
460+
},
461+
openInNewTab: false,
462+
};
463+
const url = await urlDrilldown.getHref(config, context);
464+
465+
expect(url).toBe('https://elastic.co?foo=head%2526shoulders');
466+
});
467+
468+
test('encodes URL when encoding is enabled', async () => {
469+
const config: Config = {
470+
url: {
471+
template: 'https://elastic.co?foo=head%26shoulders',
472+
},
473+
openInNewTab: false,
474+
encodeUrl: true,
475+
};
476+
const url = await urlDrilldown.getHref(config, context);
477+
478+
expect(url).toBe('https://elastic.co?foo=head%2526shoulders');
479+
});
480+
481+
test('does not encode URL when encoding is not enabled', async () => {
482+
const config: Config = {
483+
url: {
484+
template: 'https://elastic.co?foo=head%26shoulders',
485+
},
486+
openInNewTab: false,
487+
encodeUrl: false,
488+
};
489+
const url = await urlDrilldown.getHref(config, context);
490+
491+
expect(url).toBe('https://elastic.co?foo=head%26shoulders');
492+
});
493+
494+
test('can encode URI component using "encodeURIComponent" Handlebars helper', async () => {
495+
const config: Config = {
496+
url: {
497+
template: 'https://elastic.co?foo={{encodeURIComponent "head%26shoulders@gmail.com"}}',
498+
},
499+
openInNewTab: false,
500+
encodeUrl: false,
501+
};
502+
const url = await urlDrilldown.getHref(config, context);
503+
504+
expect(url).toBe('https://elastic.co?foo=head%2526shoulders%40gmail.com');
505+
});
506+
507+
test('can encode URI component using "encodeURIQuery" Handlebars helper', async () => {
508+
const config: Config = {
509+
url: {
510+
template: 'https://elastic.co?foo={{encodeURIQuery "head%26shoulders@gmail.com"}}',
511+
},
512+
openInNewTab: false,
513+
encodeUrl: false,
514+
};
515+
const url = await urlDrilldown.getHref(config, context);
516+
517+
expect(url).toBe('https://elastic.co?foo=head%2526shoulders@gmail.com');
518+
});
519+
});

x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ export class UrlDrilldown implements Drilldown<Config, ActionContext, ActionFact
104104

105105
public readonly createConfig = () => ({
106106
url: { template: '' },
107-
openInNewTab: false,
107+
openInNewTab: true,
108+
encodeUrl: true,
108109
});
109110

110111
public readonly isConfigValid = (config: Config): config is Config => {
@@ -133,7 +134,12 @@ export class UrlDrilldown implements Drilldown<Config, ActionContext, ActionFact
133134
};
134135

135136
private buildUrl(config: Config, context: ActionContext): string {
136-
const url = urlDrilldownCompileUrl(config.url.template, this.getRuntimeVariables(context));
137+
const doEncode = config.encodeUrl ?? true;
138+
const url = urlDrilldownCompileUrl(
139+
config.url.template,
140+
this.getRuntimeVariables(context),
141+
doEncode
142+
);
137143
return url;
138144
}
139145

x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const txtAddVariableButtonTitle = i18n.translate(
3434
export const txtUrlTemplateLabel = i18n.translate(
3535
'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateLabel',
3636
{
37-
defaultMessage: 'Enter URL template:',
37+
defaultMessage: 'Enter URL:',
3838
}
3939
);
4040

@@ -76,6 +76,27 @@ export const txtUrlTemplatePreviewLinkText = i18n.translate(
7676
export const txtUrlTemplateOpenInNewTab = i18n.translate(
7777
'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.openInNewTabLabel',
7878
{
79-
defaultMessage: 'Open in new tab',
79+
defaultMessage: 'Open in new window',
80+
}
81+
);
82+
83+
export const txtUrlTemplateAdditionalOptions = i18n.translate(
84+
'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.additionalOptions',
85+
{
86+
defaultMessage: 'Additional options',
87+
}
88+
);
89+
90+
export const txtUrlTemplateEncodeUrl = i18n.translate(
91+
'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.encodeUrl',
92+
{
93+
defaultMessage: 'Encode URL',
94+
}
95+
);
96+
97+
export const txtUrlTemplateEncodeDescription = i18n.translate(
98+
'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.encodeDescription',
99+
{
100+
defaultMessage: 'If enabled, URL will be escaped using percent encoding',
80101
}
81102
);

x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
import React, { useRef, useState } from 'react';
88
import {
9-
EuiCheckbox,
109
EuiFormRow,
1110
EuiIcon,
1211
EuiLink,
@@ -17,6 +16,11 @@ import {
1716
EuiText,
1817
EuiTextArea,
1918
EuiSelectableOption,
19+
EuiSwitch,
20+
EuiAccordion,
21+
EuiSpacer,
22+
EuiPanel,
23+
EuiTextColor,
2024
} from '@elastic/eui';
2125
import { UrlDrilldownConfig } from '../../types';
2226
import './index.scss';
@@ -28,6 +32,9 @@ import {
2832
txtUrlTemplateLabel,
2933
txtUrlTemplateOpenInNewTab,
3034
txtUrlTemplatePlaceholder,
35+
txtUrlTemplateAdditionalOptions,
36+
txtUrlTemplateEncodeUrl,
37+
txtUrlTemplateEncodeDescription,
3138
} from './i18n';
3239

3340
export interface UrlDrilldownCollectConfig {
@@ -110,15 +117,39 @@ export const UrlDrilldownCollectConfig: React.FC<UrlDrilldownCollectConfig> = ({
110117
inputRef={textAreaRef}
111118
/>
112119
</EuiFormRow>
113-
<EuiFormRow hasChildLabel={false}>
114-
<EuiCheckbox
115-
id="openInNewTab"
116-
name="openInNewTab"
117-
label={txtUrlTemplateOpenInNewTab}
118-
checked={config.openInNewTab}
119-
onChange={() => onConfig({ ...config, openInNewTab: !config.openInNewTab })}
120-
/>
121-
</EuiFormRow>
120+
<EuiSpacer size={'l'} />
121+
<EuiAccordion
122+
id="accordion_url_drilldown_additional_options"
123+
buttonContent={txtUrlTemplateAdditionalOptions}
124+
>
125+
<EuiSpacer size={'s'} />
126+
<EuiPanel color="subdued" borderRadius="none" hasShadow={false} style={{ border: 'none' }}>
127+
<EuiFormRow hasChildLabel={false}>
128+
<EuiSwitch
129+
id="openInNewTab"
130+
name="openInNewTab"
131+
label={txtUrlTemplateOpenInNewTab}
132+
checked={config.openInNewTab}
133+
onChange={() => onConfig({ ...config, openInNewTab: !config.openInNewTab })}
134+
/>
135+
</EuiFormRow>
136+
<EuiFormRow hasChildLabel={false} fullWidth>
137+
<EuiSwitch
138+
id="encodeUrl"
139+
name="encodeUrl"
140+
label={
141+
<>
142+
{txtUrlTemplateEncodeUrl}
143+
<EuiSpacer size={'s'} />
144+
<EuiTextColor color="subdued">{txtUrlTemplateEncodeDescription}</EuiTextColor>
145+
</>
146+
}
147+
checked={config.encodeUrl ?? true}
148+
onChange={() => onConfig({ ...config, encodeUrl: !(config.encodeUrl ?? true) })}
149+
/>
150+
</EuiFormRow>
151+
</EuiPanel>
152+
</EuiAccordion>
122153
</>
123154
);
124155
};

x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
export type UrlDrilldownConfig = {
88
url: { format?: 'handlebars_v1'; template: string };
99
openInNewTab: boolean;
10+
encodeUrl?: boolean;
1011
};
1112

1213
/**

x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,18 @@ test('should compile url without variables', () => {
1212
expect(compile(url, {})).toBe(url);
1313
});
1414

15+
test('by default, encodes URI', () => {
16+
const url = 'https://elastic.co?foo=head%26shoulders';
17+
expect(compile(url, {})).not.toBe(url);
18+
expect(compile(url, {})).toBe('https://elastic.co?foo=head%2526shoulders');
19+
});
20+
21+
test('when URI encoding is disabled, should not encode URI', () => {
22+
const url =
23+
'https://xxxxx.service-now.com/nav_to.do?uri=incident.do%3Fsys_id%3D-1%26sysparm_query%3Dshort_description%3DHello';
24+
expect(compile(url, {}, false)).toBe(url);
25+
});
26+
1527
test('should fail on unknown syntax', () => {
1628
const url = 'https://elastic.co/{{}';
1729
expect(() => compile(url, {})).toThrowError();

x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { encode, RisonValue } from 'rison-node';
99
import dateMath from '@elastic/datemath';
1010
import moment, { Moment } from 'moment';
1111
import numeral from '@elastic/numeral';
12+
import { url } from '../../../../../../src/plugins/kibana_utils/public';
1213

1314
const handlebars = createHandlebars();
1415

@@ -116,7 +117,22 @@ handlebars.registerHelper('replace', (...args) => {
116117
return String(str).split(searchString).join(valueString);
117118
});
118119

119-
export function compile(url: string, context: object): string {
120-
const template = handlebars.compile(url, { strict: true, noEscape: true });
121-
return encodeURI(template(context));
120+
handlebars.registerHelper('encodeURIComponent', (component: unknown) => {
121+
const str = String(component);
122+
return encodeURIComponent(str);
123+
});
124+
handlebars.registerHelper('encodeURIQuery', (component: unknown) => {
125+
const str = String(component);
126+
return url.encodeUriQuery(str);
127+
});
128+
129+
export function compile(urlTemplate: string, context: object, doEncode: boolean = true): string {
130+
const handlebarsTemplate = handlebars.compile(urlTemplate, { strict: true, noEscape: true });
131+
let processedUrl: string = handlebarsTemplate(context);
132+
133+
if (doEncode) {
134+
processedUrl = encodeURI(processedUrl);
135+
}
136+
137+
return processedUrl;
122138
}

x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
2525
await PageObjects.dashboard.preserveCrossAppState();
2626
});
2727

28-
it('should create dashboard to URL drilldown and use it to navigate to discover', async () => {
28+
it.skip('should create dashboard to URL drilldown and use it to navigate to discover', async () => {
2929
await PageObjects.dashboard.gotoDashboardEditMode(
3030
dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME
3131
);

0 commit comments

Comments
 (0)