mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Add uri decode to es_ui_shared and fix navigation issues with special characters (#80835)
* 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>
This commit is contained in:
parent
415a90fb69
commit
2a94139d30
31 changed files with 220 additions and 165 deletions
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
});
|
32
src/plugins/es_ui_shared/public/url/attempt_to_uri_decode.ts
Normal file
32
src/plugins/es_ui_shared/public/url/attempt_to_uri_decode.ts
Normal 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;
|
||||
};
|
|
@ -18,3 +18,4 @@
|
|||
*/
|
||||
|
||||
export { extractQueryParams } from './extract_query_params';
|
||||
export { attemptToURIDecode } from './attempt_to_uri_decode';
|
||||
|
|
|
@ -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)}`,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)]);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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)}`
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -11,5 +11,3 @@ export * from './request';
|
|||
export * from './documentation';
|
||||
|
||||
export * from './breadcrumbs';
|
||||
|
||||
export { attemptToDecodeURI } from './utils';
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -20,6 +20,7 @@ export {
|
|||
NotAuthorizedSection,
|
||||
Forms,
|
||||
GlobalFlyout,
|
||||
attemptToURIDecode,
|
||||
} from '../../../../../../../src/plugins/es_ui_shared/public';
|
||||
|
||||
export {
|
||||
|
|
|
@ -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}`);
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
})}
|
||||
>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 && (
|
||||
<>
|
||||
|
||||
|
@ -303,8 +298,7 @@ export const TemplateDetailsContent = ({
|
|||
defaultMessage: 'Delete',
|
||||
}),
|
||||
icon: 'trash',
|
||||
onClick: () =>
|
||||
setTemplateToDelete([{ name: decodedTemplateName, isLegacy }]),
|
||||
onClick: () => setTemplateToDelete([{ name: templateName, isLegacy }]),
|
||||
disabled: isCloudManaged,
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -14,6 +14,7 @@ export {
|
|||
Forms,
|
||||
extractQueryParams,
|
||||
GlobalFlyout,
|
||||
attemptToURIDecode,
|
||||
} from '../../../../src/plugins/es_ui_shared/public';
|
||||
|
||||
export {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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';
|
|
@ -24,6 +24,7 @@ export {
|
|||
XJson,
|
||||
JsonEditor,
|
||||
OnJsonEditorUpdateHandler,
|
||||
attemptToURIDecode,
|
||||
} from '../../../../src/plugins/es_ui_shared/public/';
|
||||
|
||||
export {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue