Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.x] Improve URL drilldown authoring experience (#197454) #198796

Merged
merged 1 commit into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions src/plugins/ui_actions_enhanced/.eslintrc.json

This file was deleted.

3 changes: 3 additions & 0 deletions src/plugins/ui_actions_enhanced/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { SerializableRecord } from '@kbn/utility-types';

export type BaseActionConfig = SerializableRecord;

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type SerializedAction<Config extends BaseActionConfig = BaseActionConfig> = {
readonly factoryId: string;
readonly name: string;
Expand All @@ -20,12 +21,14 @@ export type SerializedAction<Config extends BaseActionConfig = BaseActionConfig>
/**
* Serialized representation of a triggers-action pair, used to persist in storage.
*/
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type SerializedEvent = {
eventId: string;
triggers: string[];
action: SerializedAction;
};

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type DynamicActionsState = {
events: SerializedEvent[];
};
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const dashboards = [
{ id: 'dashboard2', title: 'Dashboard 2' },
];

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type DashboardDrilldownConfig = {
dashboardId?: string;
useCurrentFilters: boolean;
Expand Down Expand Up @@ -119,6 +120,7 @@ export const dashboardFactory = new ActionFactory(dashboardDrilldownActionFactor
getFeatureUsageStart: () => licensingMock.createStart().featureUsage,
});

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type UrlDrilldownConfig = {
url: string;
openInNewTab: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,48 +9,50 @@

import { i18n } from '@kbn/i18n';

export const txtUrlTemplatePlaceholder = i18n.translate(
'uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplatePlaceholderText',
{
defaultMessage: 'Example: {exampleUrl}',
values: {
exampleUrl: 'https://www.my-url.com/?{{event.key}}={{event.value}}',
},
}
);

export const txtUrlPreviewHelpText = i18n.translate(
'uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewHelpText',
{
defaultMessage: `Please note that in preview '{{event.*}}' variables are substituted with dummy values.`,
}
);

export const txtUrlTemplateLabel = i18n.translate(
'uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateLabel',
{
defaultMessage: 'Enter URL',
}
);

export const txtUrlTemplateSyntaxHelpLinkText = i18n.translate(
'uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateSyntaxHelpLinkText',
export const txtEmptyErrorMessage = i18n.translate(
'uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateEmptyErrorMessage',
{
defaultMessage: 'Syntax help',
defaultMessage: 'URL template is required.',
}
);

export const txtUrlTemplatePreviewLabel = i18n.translate(
'uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLabel',
export const txtInvalidFormatErrorMessage = ({
error,
example,
}: {
error: string;
example: string;
}) =>
i18n.translate(
'uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateInvalidFormatErrorMessage',
{
defaultMessage: '{error} Example: {example}',
values: {
error,
example,
},
}
);

export const txtUrlTemplateSyntaxTestingHelpText = i18n.translate(
'uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateSyntaxTestingHelpText',
{
defaultMessage: 'URL preview:',
defaultMessage:
'To validate and test the URL template, save the configuration and use this drilldown from the panel.',
}
);

export const txtUrlTemplatePreviewLinkText = i18n.translate(
'uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLinkText',
export const txtUrlTemplateSyntaxHelpLinkText = i18n.translate(
'uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateSyntaxHelpLinkText',
{
defaultMessage: 'Preview',
defaultMessage: 'Syntax help',
}
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@ import {
txtUrlTemplateSyntaxHelpLinkText,
txtUrlTemplateLabel,
txtUrlTemplateAdditionalOptions,
txtEmptyErrorMessage,
txtInvalidFormatErrorMessage,
txtUrlTemplateSyntaxTestingHelpText,
} from './i18n';
import { VariablePopover } from '../variable_popover';
import { UrlDrilldownOptionsComponent } from './lazy';
import { DEFAULT_URL_DRILLDOWN_OPTIONS } from '../../constants';
import { validateUrl } from '../../url_validation';

export interface UrlDrilldownCollectConfigProps {
config: UrlDrilldownConfig;
Expand Down Expand Up @@ -69,7 +73,16 @@ export const UrlDrilldownCollectConfig: React.FC<UrlDrilldownCollectConfigProps>
}
}
const isEmpty = !urlTemplate;
const isInvalid = !isPristine && isEmpty;

const isValidUrlFormat = validateUrl(urlTemplate);
const isInvalid = !isPristine && (isEmpty || !isValidUrlFormat.isValid);

const invalidErrorMessage = isInvalid
? isEmpty
? txtEmptyErrorMessage
: txtInvalidFormatErrorMessage({ error: isValidUrlFormat.error!, example: exampleUrl })
: undefined;

const variablesDropdown = (
<VariablePopover
variables={variables}
Expand All @@ -91,14 +104,18 @@ export const UrlDrilldownCollectConfig: React.FC<UrlDrilldownCollectConfigProps>
<EuiFormRow
fullWidth
isInvalid={isInvalid}
error={invalidErrorMessage}
className={'uaeUrlDrilldownCollectConfig__urlTemplateFormRow'}
label={txtUrlTemplateLabel}
helpText={
syntaxHelpDocsLink && (
<EuiLink external target={'_blank'} href={syntaxHelpDocsLink}>
{txtUrlTemplateSyntaxHelpLinkText}
</EuiLink>
)
<>
{txtUrlTemplateSyntaxTestingHelpText}{' '}
{syntaxHelpDocsLink ? (
<EuiLink external target={'_blank'} href={syntaxHelpDocsLink}>
{txtUrlTemplateSyntaxHelpLinkText}
</EuiLink>
) : null}
</>
}
labelAppend={variablesDropdown}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type UrlDrilldownConfig = {
/**
* User-configurable options for URL drilldowns
*/
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type UrlDrilldownOptions = {
openInNewTab: boolean;
encodeUrl: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,24 @@ import { compile } from './url_template';
const generalFormatError = i18n.translate(
'uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatGeneralErrorMessage',
{
defaultMessage: 'Invalid format. Example: {exampleUrl}',
values: {
exampleUrl: 'https://www.my-url.com/?{{event.key}}={{event.value}}',
},
defaultMessage: 'Invalid URL format.',
}
);

const formatError = (message: string) =>
i18n.translate('uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatErrorMessage', {
defaultMessage: 'Invalid format: {message}',
const compileError = (message: string) =>
i18n.translate('uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlCompileErrorMessage', {
defaultMessage: 'The URL template is not valid in the given context. {message}.',
values: {
message,
message: message.replaceAll('[object Object]', 'context'),
},
});

const SAFE_URL_PATTERN = /^(?:(?:https?|mailto):|[^&:/?#]*(?:[/?#]|$))/gi;
export function validateUrl(url: string): { isValid: boolean; error?: string } {
export function validateUrl(url: string): {
isValid: boolean;
error?: string;
invalidUrl?: string;
} {
if (!url)
return {
isValid: false,
Expand All @@ -45,27 +46,40 @@ export function validateUrl(url: string): { isValid: boolean; error?: string } {
return {
isValid: false,
error: generalFormatError,
invalidUrl: url,
};
}
}

export async function validateUrlTemplate(
urlTemplate: UrlDrilldownConfig['url'],
scope: UrlDrilldownScope
): Promise<{ isValid: boolean; error?: string }> {
): Promise<{ isValid: boolean; error?: string; invalidUrl?: string }> {
if (!urlTemplate.template)
return {
isValid: false,
error: generalFormatError,
};

let compiledUrl: string;

try {
compiledUrl = await compile(urlTemplate.template, scope);
} catch (e) {
return {
isValid: false,
error: compileError(e.message),
invalidUrl: urlTemplate.template,
};
}

try {
const compiledUrl = await compile(urlTemplate.template, scope);
return validateUrl(compiledUrl);
} catch (e) {
return {
isValid: false,
error: formatError(e.message),
error: generalFormatError + ` ${e.message}.`,
invalidUrl: compiledUrl,
};
}
}
Loading