Add uri decode to es_ui_shared and fix navigation issues with special characters (#80835) (#81897)

* Add uri decode to es_ui_shared and fix data stream issue with % name

* Add a test for data streams tab opened for name with a dollar sign

* Import uri decode function from es_ui_shared and fix navigation issues for filters

* Add tests for data streams with special characters in name

* Revert react router navigate changes (is done in a separate PR)

* Reverting changes to dataManagement es client and get data stream api route

* Fix data stream name filter when activated from a url parameter

* Clean up for better consistency and fixes after
https://github.com/elastic/kibana/pull/81664

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Yulia Čech 2020-10-28 16:02:21 +01:00 committed by GitHub
parent 188635f764
commit e22f2b482d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 220 additions and 165 deletions

View file

@ -57,7 +57,7 @@ export {
export { Forms, ace, GlobalFlyout, XJson };
export { extractQueryParams } from './url';
export { extractQueryParams, attemptToURIDecode } from './url';
/** dummy plugin, we just want esUiShared to have its own bundle */
export function plugin() {

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 { attemptToURIDecode } from './attempt_to_uri_decode';
test('decodes an encoded string', () => {
const encodedString = 'test%3F';
expect(attemptToURIDecode(encodedString)).toBe('test?');
});
// react router partially decodes %25 sequence to % in match params
// https://github.com/elastic/kibana/pull/81664
test('ignores the error if a string is already decoded', () => {
const decodedString = 'test%';
expect(attemptToURIDecode(decodedString)).toBe(decodedString);
});

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.
*/
/*
* Use this function with any match params coming from react router to safely decode values.
* https://github.com/elastic/kibana/pull/81664
*/
export const attemptToURIDecode = (value: string) => {
let result = value;
try {
result = decodeURIComponent(value);
} catch (e) {
// do nothing
}
return result;
};

View file

@ -18,3 +18,4 @@
*/
export { extractQueryParams } from './extract_query_params';
export { attemptToURIDecode } from './attempt_to_uri_decode';

View file

@ -193,7 +193,7 @@ export const TableContent: React.FunctionComponent<Props> = ({
icon: 'list',
onClick: () => {
navigateToApp('management', {
path: `/data/index_management${getIndexListUri(`ilm.policy:${policy.name}`, true)}`,
path: `/data/index_management${getIndexListUri(`ilm.policy:"${policy.name}"`, true)}`,
});
},
});

View file

@ -186,3 +186,28 @@ export const createDataStreamPayload = (name: string): DataStream => ({
storageSize: '1b',
maxTimeStamp: 420,
});
export const createDataStreamBackingIndex = (indexName: string, dataStreamName: string) => ({
health: '',
status: '',
primary: '',
replica: '',
documents: '',
documents_deleted: '',
size: '',
primary_size: '',
name: indexName,
data_stream: dataStreamName,
});
export const createNonDataStreamIndex = (name: string) => ({
health: 'green',
status: 'open',
primary: 1,
replica: 1,
documents: 10000,
documents_deleted: 100,
size: '156kb',
primary_size: '156kb',
name,
});

View file

@ -9,7 +9,13 @@ import { act } from 'react-dom/test-utils';
import { API_BASE_PATH } from '../../../common/constants';
import { setupEnvironment } from '../helpers';
import { DataStreamsTabTestBed, setup, createDataStreamPayload } from './data_streams_tab.helpers';
import {
DataStreamsTabTestBed,
setup,
createDataStreamPayload,
createDataStreamBackingIndex,
createNonDataStreamIndex,
} from './data_streams_tab.helpers';
describe('Data Streams tab', () => {
const { server, httpRequestsMockHelpers } = setupEnvironment();
@ -85,29 +91,8 @@ describe('Data Streams tab', () => {
} = httpRequestsMockHelpers;
setLoadIndicesResponse([
{
health: '',
status: '',
primary: '',
replica: '',
documents: '',
documents_deleted: '',
size: '',
primary_size: '',
name: 'data-stream-index',
data_stream: 'dataStream1',
},
{
health: 'green',
status: 'open',
primary: 1,
replica: 1,
documents: 10000,
documents_deleted: 100,
size: '156kb',
primary_size: '156kb',
name: 'non-data-stream-index',
},
createDataStreamBackingIndex('data-stream-index', 'dataStream1'),
createNonDataStreamIndex('non-data-stream-index'),
]);
const dataStreamForDetailPanel = createDataStreamPayload('dataStream1');
@ -260,4 +245,46 @@ describe('Data Streams tab', () => {
});
});
});
describe('when there are special characters', () => {
beforeEach(async () => {
const {
setLoadIndicesResponse,
setLoadDataStreamsResponse,
setLoadDataStreamResponse,
} = httpRequestsMockHelpers;
setLoadIndicesResponse([
createDataStreamBackingIndex('data-stream-index', '%dataStream'),
createDataStreamBackingIndex('data-stream-index2', 'dataStream2'),
]);
const dataStreamDollarSign = createDataStreamPayload('%dataStream');
setLoadDataStreamsResponse([dataStreamDollarSign]);
setLoadDataStreamResponse(dataStreamDollarSign);
testBed = await setup();
await act(async () => {
testBed.actions.goToDataStreamsList();
});
testBed.component.update();
});
describe('detail panel', () => {
test('opens when the data stream name in the table is clicked', async () => {
const { actions, findDetailPanel, findDetailPanelTitle } = testBed;
await actions.clickNameAt(0);
expect(findDetailPanel().length).toBe(1);
expect(findDetailPanelTitle()).toBe('%dataStream');
});
test('clicking the indices count navigates to the backing indices', async () => {
const { table, actions } = testBed;
await actions.clickIndicesAt(0);
expect(table.getMetaData('indexTable').tableCellsValues).toEqual([
['', '', '', '', '', '', '', '%dataStream'],
]);
});
});
});
});

View file

@ -20,12 +20,17 @@ import {
EuiBadge,
} from '@elastic/eui';
import { SectionLoading, TabSettings, TabAliases, TabMappings } from '../shared_imports';
import {
SectionLoading,
TabSettings,
TabAliases,
TabMappings,
attemptToURIDecode,
} from '../shared_imports';
import { useComponentTemplatesContext } from '../component_templates_context';
import { TabSummary } from './tab_summary';
import { ComponentTemplateTabs, TabType } from './tabs';
import { ManageButton, ManageAction } from './manage_button';
import { attemptToDecodeURI } from '../lib';
export interface Props {
componentTemplateName: string;
@ -47,7 +52,7 @@ export const ComponentTemplateDetailsFlyoutContent: React.FunctionComponent<Prop
}) => {
const { api } = useComponentTemplatesContext();
const decodedComponentTemplateName = attemptToDecodeURI(componentTemplateName);
const decodedComponentTemplateName = attemptToURIDecode(componentTemplateName);
const { data: componentTemplateDetails, isLoading, error } = api.useLoadComponentTemplate(
decodedComponentTemplateName

View file

@ -11,9 +11,9 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { ScopedHistory } from 'kibana/public';
import { EuiLink, EuiText, EuiSpacer } from '@elastic/eui';
import { attemptToURIDecode } from '../../../../shared_imports';
import { SectionLoading, ComponentTemplateDeserialized, GlobalFlyout } from '../shared_imports';
import { UIM_COMPONENT_TEMPLATE_LIST_LOAD } from '../constants';
import { attemptToDecodeURI } from '../lib';
import { useComponentTemplatesContext } from '../component_templates_context';
import {
ComponentTemplateDetailsFlyoutContent,
@ -84,7 +84,7 @@ export const ComponentTemplateList: React.FunctionComponent<Props> = ({
}),
icon: 'pencil',
handleActionClick: () =>
goToEditComponentTemplate(attemptToDecodeURI(componentTemplateName)),
goToEditComponentTemplate(attemptToURIDecode(componentTemplateName)),
},
{
name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.cloneActionLabel', {
@ -92,7 +92,7 @@ export const ComponentTemplateList: React.FunctionComponent<Props> = ({
}),
icon: 'copy',
handleActionClick: () =>
goToCloneComponentTemplate(attemptToDecodeURI(componentTemplateName)),
goToCloneComponentTemplate(attemptToURIDecode(componentTemplateName)),
},
{
name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.deleteButtonLabel', {
@ -103,7 +103,7 @@ export const ComponentTemplateList: React.FunctionComponent<Props> = ({
details._kbnMeta.usedBy.length > 0,
closePopoverOnClick: true,
handleActionClick: () => {
setComponentTemplatesToDelete([attemptToDecodeURI(componentTemplateName)]);
setComponentTemplatesToDelete([attemptToURIDecode(componentTemplateName)]);
},
},
];

View file

@ -9,9 +9,8 @@ import { RouteComponentProps } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { SectionLoading } from '../../shared_imports';
import { SectionLoading, attemptToURIDecode } from '../../shared_imports';
import { useComponentTemplatesContext } from '../../component_templates_context';
import { attemptToDecodeURI } from '../../lib';
import { ComponentTemplateCreate } from '../component_template_create';
export interface Params {
@ -20,7 +19,7 @@ export interface Params {
export const ComponentTemplateClone: FunctionComponent<RouteComponentProps<Params>> = (props) => {
const { sourceComponentTemplateName } = props.match.params;
const decodedSourceName = attemptToDecodeURI(sourceComponentTemplateName);
const decodedSourceName = attemptToURIDecode(sourceComponentTemplateName);
const { toasts, api } = useComponentTemplatesContext();

View file

@ -9,8 +9,11 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@elastic/eui';
import { useComponentTemplatesContext } from '../../component_templates_context';
import { ComponentTemplateDeserialized, SectionLoading } from '../../shared_imports';
import { attemptToDecodeURI } from '../../lib';
import {
ComponentTemplateDeserialized,
SectionLoading,
attemptToURIDecode,
} from '../../shared_imports';
import { ComponentTemplateForm } from '../component_template_form';
interface MatchParams {
@ -28,7 +31,7 @@ export const ComponentTemplateEdit: React.FunctionComponent<RouteComponentProps<
const [isSaving, setIsSaving] = useState<boolean>(false);
const [saveError, setSaveError] = useState<any>(null);
const decodedName = attemptToDecodeURI(name);
const decodedName = attemptToURIDecode(name);
const { error, data: componentTemplate, isLoading } = api.useLoadComponentTemplate(decodedName);
@ -50,7 +53,9 @@ export const ComponentTemplateEdit: React.FunctionComponent<RouteComponentProps<
}
history.push({
pathname: encodeURI(`/component_templates/${encodeURIComponent(name)}`),
pathname: encodeURI(
`/component_templates/${encodeURIComponent(updatedComponentTemplate.name)}`
),
});
};

View file

@ -11,5 +11,3 @@ export * from './request';
export * from './documentation';
export * from './breadcrumbs';
export { attemptToDecodeURI } from './utils';

View file

@ -1,18 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const attemptToDecodeURI = (value: string) => {
let result: string;
try {
result = decodeURI(value);
result = decodeURIComponent(result);
} catch (e) {
result = decodeURIComponent(value);
}
return result;
};

View file

@ -20,6 +20,7 @@ export {
NotAuthorizedSection,
Forms,
GlobalFlyout,
attemptToURIDecode,
} from '../../../../../../../src/plugins/es_ui_shared/public';
export {

View file

@ -20,13 +20,17 @@ import {
} from '@elastic/eui';
import { ScopedHistory } from 'kibana/public';
import { reactRouterNavigate, extractQueryParams } from '../../../../shared_imports';
import {
reactRouterNavigate,
extractQueryParams,
attemptToURIDecode,
} from '../../../../shared_imports';
import { useAppContext } from '../../../app_context';
import { SectionError, SectionLoading, Error } from '../../../components';
import { useLoadDataStreams } from '../../../services/api';
import { encodePathForReactRouter, decodePathFromReactRouter } from '../../../services/routing';
import { getIndexListUri } from '../../../services/routing';
import { documentationService } from '../../../services/documentation';
import { Section } from '../../home';
import { Section } from '../home';
import { DataStreamTable } from './data_stream_table';
import { DataStreamDetailPanel } from './data_stream_detail_panel';
@ -206,7 +210,7 @@ export const DataStreamList: React.FunctionComponent<RouteComponentProps<MatchPa
<DataStreamTable
filters={
isDeepLink && dataStreamName !== undefined
? `name=${decodePathFromReactRouter(dataStreamName)}`
? `name="${attemptToURIDecode(dataStreamName)}"`
: ''
}
dataStreams={dataStreams}
@ -228,13 +232,11 @@ export const DataStreamList: React.FunctionComponent<RouteComponentProps<MatchPa
*/}
{dataStreamName && (
<DataStreamDetailPanel
dataStreamName={decodePathFromReactRouter(dataStreamName)}
backingIndicesLink={reactRouterNavigate(history, {
pathname: '/indices',
search: `includeHiddenIndices=true&filter=data_stream=${encodePathForReactRouter(
dataStreamName
)}`,
})}
dataStreamName={attemptToURIDecode(dataStreamName)}
backingIndicesLink={reactRouterNavigate(
history,
getIndexListUri(`data_stream="${attemptToURIDecode(dataStreamName)}"`, true)
)}
onClose={(shouldReload?: boolean) => {
history.push(`/${Section.DataStreams}`);

View file

@ -12,9 +12,8 @@ import { ScopedHistory } from 'kibana/public';
import { DataStream } from '../../../../../../common/types';
import { UseRequestResponse, reactRouterNavigate } from '../../../../../shared_imports';
import { encodePathForReactRouter } from '../../../../services/routing';
import { getDataStreamDetailsLink, getIndexListUri } from '../../../../services/routing';
import { DataHealth } from '../../../../components';
import { Section } from '../../../home';
import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal';
import { humanizeTimeStamp } from '../humanize_time_stamp';
@ -45,13 +44,11 @@ export const DataStreamTable: React.FunctionComponent<Props> = ({
}),
truncateText: true,
sortable: true,
render: (name: DataStream['name'], item: DataStream) => {
render: (name: DataStream['name']) => {
return (
<EuiLink
data-test-subj="nameLink"
{...reactRouterNavigate(history, {
pathname: `/${Section.DataStreams}/${encodePathForReactRouter(name)}`,
})}
{...reactRouterNavigate(history, getDataStreamDetailsLink(name))}
>
{name}
</EuiLink>
@ -108,12 +105,7 @@ export const DataStreamTable: React.FunctionComponent<Props> = ({
render: (indices: DataStream['indices'], dataStream) => (
<EuiLink
data-test-subj="indicesLink"
{...reactRouterNavigate(history, {
pathname: '/indices',
search: `includeHiddenIndices=true&filter=data_stream=${encodePathForReactRouter(
dataStream.name
)}`,
})}
{...reactRouterNavigate(history, getIndexListUri(`data_stream="${dataStream.name}"`, true))}
>
{indices.length}
</EuiLink>

View file

@ -35,9 +35,9 @@ import {
} from '@elastic/eui';
import { UIM_SHOW_DETAILS_CLICK } from '../../../../../../common/constants';
import { reactRouterNavigate } from '../../../../../shared_imports';
import { reactRouterNavigate, attemptToURIDecode } from '../../../../../shared_imports';
import { REFRESH_RATE_INDEX_LIST } from '../../../../constants';
import { encodePathForReactRouter } from '../../../../services/routing';
import { getDataStreamDetailsLink } from '../../../../services/routing';
import { documentationService } from '../../../../services/documentation';
import { AppContextConsumer } from '../../../../app_context';
import { renderBadges } from '../../../../lib/render_badges';
@ -107,7 +107,7 @@ export class IndexTable extends Component {
const { location, filterChanged } = this.props;
const { filter } = qs.parse((location && location.search) || '');
if (filter) {
const decodedFilter = decodeURIComponent(filter);
const decodedFilter = attemptToURIDecode(filter);
try {
const filter = EuiSearchBar.Query.parse(decodedFilter);
@ -279,7 +279,7 @@ export class IndexTable extends Component {
<EuiLink
data-test-subj="dataStreamLink"
{...reactRouterNavigate(history, {
pathname: `/data_streams/${encodePathForReactRouter(value)}`,
pathname: getDataStreamDetailsLink(value),
search: '?isDeepLink=true',
})}
>

View file

@ -13,7 +13,7 @@ import { UseRequestResponse, reactRouterNavigate } from '../../../../../../share
import { TemplateListItem } from '../../../../../../../common';
import { UIM_TEMPLATE_SHOW_DETAILS_CLICK } from '../../../../../../../common/constants';
import { TemplateDeleteModal } from '../../../../../components';
import { encodePathForReactRouter } from '../../../../../services/routing';
import { getTemplateDetailsLink } from '../../../../../services/routing';
import { useServices } from '../../../../../app_context';
import { TemplateContentIndicator } from '../../../../../components/shared';
import { TemplateTypeIndicator } from '../../components';
@ -53,10 +53,7 @@ export const LegacyTemplateTable: React.FunctionComponent<Props> = ({
<EuiLink
{...reactRouterNavigate(
history,
{
pathname: `/templates/${encodePathForReactRouter(name)}`,
search: `legacy=${Boolean(item._kbnMeta.isLegacy)}`,
},
getTemplateDetailsLink(name, Boolean(item._kbnMeta.isLegacy)),
() => uiMetricService.trackMetric('click', UIM_TEMPLATE_SHOW_DETAILS_CLICK)
)}
data-test-subj="templateDetailsLink"

View file

@ -34,7 +34,6 @@ import {
import { UseRequestResponse } from '../../../../../shared_imports';
import { TemplateDeleteModal, SectionLoading, SectionError, Error } from '../../../../components';
import { useLoadIndexTemplate } from '../../../../services/api';
import { decodePathFromReactRouter } from '../../../../services/routing';
import { useServices } from '../../../../app_context';
import { TabAliases, TabMappings, TabSettings } from '../../../../components/shared';
import { TemplateTypeIndicator } from '../components';
@ -103,11 +102,7 @@ export const TemplateDetailsContent = ({
reload,
}: Props) => {
const { uiMetricService } = useServices();
const decodedTemplateName = decodePathFromReactRouter(templateName);
const { error, data: templateDetails, isLoading } = useLoadIndexTemplate(
decodedTemplateName,
isLegacy
);
const { error, data: templateDetails, isLoading } = useLoadIndexTemplate(templateName, isLegacy);
const isCloudManaged = templateDetails?._kbnMeta.type === 'cloudManaged';
const [templateToDelete, setTemplateToDelete] = useState<
Array<{ name: string; isLegacy?: boolean }>
@ -120,7 +115,7 @@ export const TemplateDetailsContent = ({
<EuiFlyoutHeader>
<EuiTitle size="m">
<h2 id="templateDetailsFlyoutTitle" data-test-subj="title">
{decodedTemplateName}
{templateName}
{templateDetails && (
<>
&nbsp;
@ -303,8 +298,7 @@ export const TemplateDetailsContent = ({
defaultMessage: 'Delete',
}),
icon: 'trash',
onClick: () =>
setTemplateToDelete([{ name: decodedTemplateName, isLegacy }]),
onClick: () => setTemplateToDelete([{ name: templateName, isLegacy }]),
disabled: isCloudManaged,
},
],

View file

@ -36,6 +36,7 @@ import { TemplateTable } from './template_table';
import { TemplateDetails } from './template_details';
import { LegacyTemplateTable } from './legacy_templates/template_table';
import { FilterListButton, Filters } from './components';
import { attemptToURIDecode } from '../../../../shared_imports';
type FilterName = 'managed' | 'cloudManaged' | 'system';
interface MatchParams {
@ -100,7 +101,7 @@ export const TemplateList: React.FunctionComponent<RouteComponentProps<MatchPara
const selectedTemplate = Boolean(templateName)
? {
name: templateName!,
name: attemptToURIDecode(templateName!),
isLegacy: getIsLegacyFromQueryParams(location),
}
: null;

View file

@ -13,11 +13,11 @@ import { ScopedHistory } from 'kibana/public';
import { TemplateListItem } from '../../../../../../common';
import { UIM_TEMPLATE_SHOW_DETAILS_CLICK } from '../../../../../../common/constants';
import { UseRequestResponse, reactRouterNavigate } from '../../../../../shared_imports';
import { encodePathForReactRouter } from '../../../../services/routing';
import { useServices } from '../../../../app_context';
import { TemplateDeleteModal } from '../../../../components';
import { TemplateContentIndicator } from '../../../../components/shared';
import { TemplateTypeIndicator } from '../components';
import { getTemplateDetailsLink } from '../../../../services/routing';
interface Props {
templates: TemplateListItem[];
@ -52,12 +52,8 @@ export const TemplateTable: React.FunctionComponent<Props> = ({
return (
<>
<EuiLink
{...reactRouterNavigate(
history,
{
pathname: `/templates/${encodePathForReactRouter(name)}`,
},
() => uiMetricService.trackMetric('click', UIM_TEMPLATE_SHOW_DETAILS_CLICK)
{...reactRouterNavigate(history, getTemplateDetailsLink(name), () =>
uiMetricService.trackMetric('click', UIM_TEMPLATE_SHOW_DETAILS_CLICK)
)}
data-test-subj="templateDetailsLink"
>

View file

@ -11,9 +11,10 @@ import { EuiPageBody, EuiPageContent, EuiTitle } from '@elastic/eui';
import { TemplateDeserialized } from '../../../../common';
import { TemplateForm, SectionLoading, SectionError, Error } from '../../components';
import { breadcrumbService } from '../../services/breadcrumbs';
import { decodePathFromReactRouter, getTemplateDetailsLink } from '../../services/routing';
import { getTemplateDetailsLink } from '../../services/routing';
import { saveTemplate, useLoadIndexTemplate } from '../../services/api';
import { getIsLegacyFromQueryParams } from '../../lib/index_templates';
import { attemptToURIDecode } from '../../../shared_imports';
interface MatchParams {
name: string;
@ -26,7 +27,7 @@ export const TemplateClone: React.FunctionComponent<RouteComponentProps<MatchPar
location,
history,
}) => {
const decodedTemplateName = decodePathFromReactRouter(name);
const decodedTemplateName = attemptToURIDecode(name);
const isLegacy = getIsLegacyFromQueryParams(location);
const [isSaving, setIsSaving] = useState<boolean>(false);

View file

@ -11,9 +11,10 @@ import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@e
import { TemplateDeserialized } from '../../../../common';
import { breadcrumbService } from '../../services/breadcrumbs';
import { useLoadIndexTemplate, updateTemplate } from '../../services/api';
import { decodePathFromReactRouter, getTemplateDetailsLink } from '../../services/routing';
import { getTemplateDetailsLink } from '../../services/routing';
import { SectionLoading, SectionError, TemplateForm, Error } from '../../components';
import { getIsLegacyFromQueryParams } from '../../lib/index_templates';
import { attemptToURIDecode } from '../../../shared_imports';
interface MatchParams {
name: string;
@ -26,7 +27,7 @@ export const TemplateEdit: React.FunctionComponent<RouteComponentProps<MatchPara
location,
history,
}) => {
const decodedTemplateName = decodePathFromReactRouter(name);
const decodedTemplateName = attemptToURIDecode(name);
const isLegacy = getIsLegacyFromQueryParams(location);
const [isSaving, setIsSaving] = useState<boolean>(false);
@ -51,7 +52,7 @@ export const TemplateEdit: React.FunctionComponent<RouteComponentProps<MatchPara
return;
}
history.push(getTemplateDetailsLink(name, updatedTemplate._kbnMeta.isLegacy));
history.push(getTemplateDetailsLink(decodedTemplateName, updatedTemplate._kbnMeta.isLegacy));
};
const clearSaveError = () => {

View file

@ -6,9 +6,8 @@
export const getTemplateListLink = () => `/templates`;
export const getTemplateDetailsLink = (name: string, isLegacy?: boolean, withHash = false) => {
const baseUrl = `/templates/${encodePathForReactRouter(name)}`;
let url = withHash ? `#${baseUrl}` : baseUrl;
export const getTemplateDetailsLink = (name: string, isLegacy?: boolean) => {
let url = `/templates/${encodeURIComponent(name)}`;
if (isLegacy) {
url = `${url}?legacy=${isLegacy}`;
}
@ -16,7 +15,7 @@ export const getTemplateDetailsLink = (name: string, isLegacy?: boolean, withHas
};
export const getTemplateEditLink = (name: string, isLegacy?: boolean) => {
let url = `/edit_template/${encodePathForReactRouter(name)}`;
let url = `/edit_template/${encodeURIComponent(name)}`;
if (isLegacy) {
url = `${url}?legacy=true`;
}
@ -24,7 +23,7 @@ export const getTemplateEditLink = (name: string, isLegacy?: boolean) => {
};
export const getTemplateCloneLink = (name: string, isLegacy?: boolean) => {
let url = `/clone_template/${encodePathForReactRouter(name)}`;
let url = `/clone_template/${encodeURIComponent(name)}`;
if (isLegacy) {
url = `${url}?legacy=true`;
}
@ -32,9 +31,7 @@ export const getTemplateCloneLink = (name: string, isLegacy?: boolean) => {
};
export const getILMPolicyPath = (policyName: string) => {
return encodeURI(
`/data/index_lifecycle_management/policies/edit/${encodeURIComponent(policyName)}`
);
return `/data/index_lifecycle_management/policies/edit/${encodeURIComponent(policyName)}`;
};
export const getIndexListUri = (filter?: string, includeHiddenIndices?: boolean) => {
@ -53,18 +50,6 @@ export const getIndexListUri = (filter?: string, includeHiddenIndices?: boolean)
return '/indices';
};
export const decodePathFromReactRouter = (pathname: string): string => {
let decodedPath;
try {
decodedPath = decodeURI(pathname);
decodedPath = decodeURIComponent(decodedPath);
} catch (_error) {
decodedPath = decodeURIComponent(pathname);
}
return decodeURIComponent(decodedPath);
export const getDataStreamDetailsLink = (name: string) => {
return encodeURI(`/data_streams/${encodeURIComponent(name)}`);
};
// Need to add some additonal encoding/decoding logic to work with React Router
// For background, see: https://github.com/ReactTraining/history/issues/505
export const encodePathForReactRouter = (pathname: string): string =>
encodeURIComponent(encodeURIComponent(pathname));

View file

@ -14,6 +14,7 @@ export {
Forms,
extractQueryParams,
GlobalFlyout,
attemptToURIDecode,
} from '../../../../src/plugins/es_ui_shared/public';
export {

View file

@ -83,15 +83,16 @@ export function registerGetOneRoute({ router, license, lib: { isEsError } }: Rou
license.guardApiRoute(async (ctx, req, res) => {
const { name } = req.params as TypeOf<typeof paramsSchema>;
const { callAsCurrentUser } = ctx.dataManagement!.client;
try {
const [
{ data_streams: dataStream },
{ data_streams: dataStreamsStats },
] = await Promise.all([
callAsCurrentUser('dataManagement.getDataStream', { name }),
callAsCurrentUser('dataManagement.getDataStream', {
name,
}),
ctx.core.elasticsearch.legacy.client.callAsCurrentUser('transport.request', {
path: `/_data_stream/${name}/_stats`,
path: `/_data_stream/${encodeURIComponent(name)}/_stats`,
method: 'GET',
query: {
human: true,

View file

@ -9,10 +9,9 @@ import { RouteComponentProps } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { SectionLoading, useKibana } from '../../../shared_imports';
import { SectionLoading, useKibana, attemptToURIDecode } from '../../../shared_imports';
import { PipelinesCreate } from '../pipelines_create';
import { attemptToURIDecode } from '../shared';
export interface ParamProps {
sourceName: string;

View file

@ -18,11 +18,10 @@ import {
import { EuiCallOut } from '@elastic/eui';
import { Pipeline } from '../../../../common/types';
import { useKibana, SectionLoading } from '../../../shared_imports';
import { useKibana, SectionLoading, attemptToURIDecode } from '../../../shared_imports';
import { getListPath } from '../../services/navigation';
import { PipelineForm } from '../../components';
import { attemptToURIDecode } from '../shared';
interface MatchParams {
name: string;

View file

@ -1,15 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const attemptToURIDecode = (value: string) => {
let result: string;
try {
result = decodeURI(decodeURIComponent(value));
} catch (e) {
result = value;
}
return result;
};

View file

@ -1,7 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { attemptToURIDecode } from './attempt_to_uri_decode';

View file

@ -24,6 +24,7 @@ export {
XJson,
JsonEditor,
OnJsonEditorUpdateHandler,
attemptToURIDecode,
} from '../../../../src/plugins/es_ui_shared/public/';
export {