diff --git a/src/plugins/vis_types/timeseries/public/application/components/lib/external_url_error_modal.tsx b/src/plugins/vis_types/timeseries/public/application/components/lib/external_url_error_modal.tsx new file mode 100644 index 0000000000000..43016e05dfd40 --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/external_url_error_modal.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiButton, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiTextColor, +} from '@elastic/eui'; + +interface ExternalUrlErrorModalProps { + url: string; + handleClose: () => void; +} + +export const ExternalUrlErrorModal = ({ url, handleClose }: ExternalUrlErrorModalProps) => ( + + + + + + + + + {url} + + ), + externalUrlPolicy: 'externalUrl.policy', + kibanaConfigFileName: 'kibana.yml', + }} + /> + + + + + + + +); diff --git a/src/plugins/vis_types/timeseries/public/application/components/vis_types/table/vis.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/table/vis.js index 7b1db4b362647..4a4db04998389 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/vis_types/table/vis.js +++ b/src/plugins/vis_types/timeseries/public/application/components/vis_types/table/vis.js @@ -17,6 +17,7 @@ import { createFieldFormatter } from '../../lib/create_field_formatter'; import { isSortable } from './is_sortable'; import { EuiToolTip, EuiIcon } from '@elastic/eui'; import { replaceVars } from '../../lib/replace_vars'; +import { ExternalUrlErrorModal } from '../../lib/external_url_error_modal'; import { FIELD_FORMAT_IDS } from '../../../../../../../../plugins/field_formats/common'; import { FormattedMessage } from '@kbn/i18n/react'; import { getFieldFormats, getCoreStart } from '../../../../services'; @@ -53,12 +54,24 @@ class TableVis extends Component { const DateFormat = fieldFormatsService.getType(FIELD_FORMAT_IDS.DATE); this.dateFormatter = new DateFormat({}, this.props.getConfig); + + this.state = { + showExternalUrlErrorModal: false, + }; } get visibleSeries() { return get(this.props, 'model.series', []).filter((series) => !series.hidden); } + handleDrilldownUrlClick = (event, url) => { + const validatedUrl = getCoreStart().http.externalUrl.validateUrl(url); + if (!validatedUrl) { + event.preventDefault(); + this.setState({ accessDeniedDrilldownUrl: url, showExternalUrlErrorModal: true }); + } + }; + renderRow = (row) => { const { model, fieldFormatMap, getConfig } = this.props; @@ -74,7 +87,11 @@ class TableVis extends Component { if (model.drilldown_url) { const url = replaceVars(model.drilldown_url, {}, { key: row.key }); - rowDisplay = {rowDisplay}; + rowDisplay = ( + this.handleDrilldownUrlClick(event, url)}> + {rowDisplay} + + ); } const columns = row.series @@ -213,6 +230,8 @@ class TableVis extends Component { ); } + closeExternalUrlErrorModal = () => this.setState({ showExternalUrlErrorModal: false }); + render() { const { visData, model } = this.props; const header = this.renderHeader(); @@ -239,16 +258,24 @@ class TableVis extends Component { ); } return ( - - - {header} - {rows} -
-
+ <> + + + {header} + {rows} +
+
+ {this.state.showExternalUrlErrorModal && ( + + )} + ); } } diff --git a/src/plugins/vis_types/timeseries/public/application/components/vis_types/top_n/vis.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/top_n/vis.js index 8176f6ece2805..fe199ecd4302f 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/vis_types/top_n/vis.js +++ b/src/plugins/vis_types/timeseries/public/application/components/vis_types/top_n/vis.js @@ -15,10 +15,11 @@ import { getLastValue } from '../../../../../common/last_value_utils'; import { isBackgroundInverted } from '../../../lib/set_is_reversed'; import { replaceVars } from '../../lib/replace_vars'; import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useState, useCallback } from 'react'; import { sortBy, first, get } from 'lodash'; import { DATA_FORMATTERS } from '../../../../../common/enums'; import { getOperator, shouldOperate } from '../../../../../common/operators_utils'; +import { ExternalUrlErrorModal } from '../../lib/external_url_error_modal'; function sortByDirection(data, direction, fn) { if (direction === 'desc') { @@ -41,6 +42,9 @@ function sortSeries(visData, model) { } function TopNVisualization(props) { + const [accessDeniedDrilldownUrl, setAccessDeniedDrilldownUrl] = useState(null); + const [showExternalUrlErrorModal, setShowExternalUrlErrorModal] = useState(false); + const coreStart = getCoreStart(); const { backgroundColor, model, visData, fieldFormatMap, getConfig } = props; const series = sortSeries(visData, model).map((item) => { @@ -83,13 +87,27 @@ function TopNVisualization(props) { if (model.drilldown_url) { params.onClick = (item) => { const url = replaceVars(model.drilldown_url, {}, { key: item.label }); - getCoreStart().application.navigateToUrl(url); + const validatedUrl = coreStart.http.externalUrl.validateUrl(url); + if (validatedUrl) { + coreStart.application.navigateToUrl(url); + } else { + setAccessDeniedDrilldownUrl(url); + setShowExternalUrlErrorModal(true); + } }; } + const closeExternalUrlErrorModal = useCallback(() => setShowExternalUrlErrorModal(false), []); + return (
+ {showExternalUrlErrorModal && ( + + )}
); }