[Maps] use index_exists route instead of /api/index_management/indices (#98479)

* [Maps] use index_exists route instead of /api/index_management/indices

* fix functional test

* add retry and correct permissions to fix functional tests

* fix upload functional test
This commit is contained in:
Nathan Reese 2021-04-28 16:05:52 -06:00 committed by GitHub
parent 9c469feb3b
commit 40fddce405
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 144 additions and 124 deletions

View file

@ -92,13 +92,17 @@ export async function checkIndexExists(
): Promise<boolean> {
const body = JSON.stringify({ index });
const fileUploadModules = await lazyLoadModules();
const { exists } = await fileUploadModules.getHttp().fetch<{ exists: boolean }>({
path: `/internal/file_upload/index_exists`,
method: 'POST',
body,
query: params,
});
return exists;
try {
const { exists } = await fileUploadModules.getHttp().fetch<{ exists: boolean }>({
path: `/internal/file_upload/index_exists`,
method: 'POST',
body,
query: params,
});
return exists;
} catch (error) {
return false;
}
}
export async function getTimeFieldRange(index: string, query: unknown, timeFieldName?: string) {

View file

@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n';
import { GeoJsonFilePicker, OnFileSelectParameters } from './geojson_file_picker';
import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/public';
import { IndexNameForm } from './index_name_form';
import { validateIndexName } from '../../util/indexing_service';
import { validateIndexName } from '../../validate_index_name';
const GEO_FIELD_TYPE_OPTIONS = [
{
@ -32,6 +32,8 @@ interface Props {
onFileSelect: (onFileSelectParameters: OnFileSelectParameters) => void;
onGeoFieldTypeSelect: (geoFieldType: ES_FIELD_TYPES.GEO_POINT | ES_FIELD_TYPES.GEO_SHAPE) => void;
onIndexNameChange: (name: string, error?: string) => void;
onIndexNameValidationStart: () => void;
onIndexNameValidationEnd: () => void;
}
interface State {
@ -40,11 +42,20 @@ interface State {
}
export class GeoJsonUploadForm extends Component<Props, State> {
private _isMounted = false;
state: State = {
hasFile: false,
isPointsOnly: false,
};
componentDidMount() {
this._isMounted = true;
}
componentWillUnmount() {
this._isMounted = false;
}
_onFileSelect = async (onFileSelectParameters: OnFileSelectParameters) => {
this.setState({
hasFile: true,
@ -53,7 +64,12 @@ export class GeoJsonUploadForm extends Component<Props, State> {
this.props.onFileSelect(onFileSelectParameters);
this.props.onIndexNameValidationStart();
const indexNameError = await validateIndexName(onFileSelectParameters.indexName);
if (!this._isMounted) {
return;
}
this.props.onIndexNameValidationEnd();
this.props.onIndexNameChange(onFileSelectParameters.indexName, indexNameError);
const geoFieldType =
@ -107,6 +123,8 @@ export class GeoJsonUploadForm extends Component<Props, State> {
indexName={this.props.indexName}
indexNameError={this.props.indexNameError}
onIndexNameChange={this.props.onIndexNameChange}
onIndexNameValidationStart={this.props.onIndexNameValidationStart}
onIndexNameValidationEnd={this.props.onIndexNameValidationEnd}
/>
) : null}
</EuiForm>

View file

@ -5,24 +5,47 @@
* 2.0.
*/
import _ from 'lodash';
import React, { ChangeEvent, Component } from 'react';
import { EuiFormRow, EuiFieldText, EuiCallOut, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { validateIndexName } from '../../util/indexing_service';
import { validateIndexName } from '../../validate_index_name';
export interface Props {
indexName: string;
indexNameError?: string;
onIndexNameChange: (name: string, error?: string) => void;
onIndexNameValidationStart: () => void;
onIndexNameValidationEnd: () => void;
}
export class IndexNameForm extends Component<Props> {
_onIndexNameChange = async (event: ChangeEvent<HTMLInputElement>) => {
private _isMounted = false;
componentDidMount() {
this._isMounted = true;
}
componentWillUnmount() {
this._isMounted = false;
}
_onIndexNameChange = (event: ChangeEvent<HTMLInputElement>) => {
const indexName = event.target.value;
const indexNameError = await validateIndexName(indexName);
this.props.onIndexNameChange(indexName, indexNameError);
this.props.onIndexNameChange(indexName);
this._validateIndexName(indexName);
this.props.onIndexNameValidationStart();
};
_validateIndexName = _.debounce(async (indexName: string) => {
const indexNameError = await validateIndexName(indexName);
if (!this._isMounted || indexName !== this.props.indexName) {
return;
}
this.props.onIndexNameValidationEnd();
this.props.onIndexNameChange(indexName, indexNameError);
}, 500);
render() {
const errors = [...(this.props.indexNameError ? [this.props.indexNameError] : [])];

View file

@ -274,7 +274,11 @@ export class JsonUploadAndParse extends Component<FileUploadComponentProps, Stat
});
const isReadyToImport = !!name && error === undefined;
this.props.onIndexReady(isReadyToImport);
if (isReadyToImport) {
this.props.enableImportBtn();
} else {
this.props.disableImportBtn();
}
};
render() {
@ -309,6 +313,8 @@ export class JsonUploadAndParse extends Component<FileUploadComponentProps, Stat
onFileSelect={this._onFileSelect}
onGeoFieldTypeSelect={this._onGeoFieldTypeSelect}
onIndexNameChange={this._onIndexNameChange}
onIndexNameValidationStart={this.props.disableImportBtn}
onIndexNameValidationEnd={this.props.enableImportBtn}
/>
);
}

View file

@ -24,7 +24,8 @@ export interface FileUploadComponentProps {
isIndexingTriggered: boolean;
onFileSelect: (geojsonFile: FeatureCollection, name: string, previewCoverage: number) => void;
onFileClear: () => void;
onIndexReady: (indexReady: boolean) => void;
enableImportBtn: () => void;
disableImportBtn: () => void;
onUploadComplete: (results: FileUploadGeoResults) => void;
onUploadError: () => void;
}

View file

@ -1,73 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import { getIndexPatternService, getHttp } from '../kibana_services';
export const getExistingIndexNames = _.debounce(
async () => {
let indexes;
try {
indexes = await getHttp().fetch({
path: `/api/index_management/indices`,
method: 'GET',
});
} catch (e) {
// Log to console. Further diagnostics can be made in network request
// eslint-disable-next-line no-console
console.error(e);
}
return indexes ? indexes.map(({ name }: { name: string }) => name) : [];
},
10000,
{ leading: true }
);
export function checkIndexPatternValid(name: string) {
const byteLength = encodeURI(name).split(/%(?:u[0-9A-F]{2})?[0-9A-F]{2}|./).length - 1;
const reg = new RegExp('[\\\\/*?"<>|\\s,#]+');
const indexPatternInvalid =
byteLength > 255 || // name can't be greater than 255 bytes
name !== name.toLowerCase() || // name should be lowercase
name === '.' ||
name === '..' || // name can't be . or ..
name.match(/^[-_+]/) !== null || // name can't start with these chars
name.match(reg) !== null; // name can't contain these chars
return !indexPatternInvalid;
}
export const validateIndexName = async (indexName: string) => {
if (!checkIndexPatternValid(indexName)) {
return i18n.translate(
'xpack.fileUpload.util.indexingService.indexNameContainsIllegalCharactersErrorMessage',
{
defaultMessage: 'Index name contains illegal characters.',
}
);
}
const indexNames = await getExistingIndexNames();
const indexPatternNames = await getIndexPatternService().getTitles();
let indexNameError;
if (indexNames.includes(indexName)) {
indexNameError = i18n.translate(
'xpack.fileUpload.util.indexingService.indexNameAlreadyExistsErrorMessage',
{
defaultMessage: 'Index name already exists.',
}
);
} else if (indexPatternNames.includes(indexName)) {
indexNameError = i18n.translate(
'xpack.fileUpload.util.indexingService.indexPatternAlreadyExistsErrorMessage',
{
defaultMessage: 'Index pattern already exists.',
}
);
}
return indexNameError;
};

View file

@ -5,12 +5,10 @@
* 2.0.
*/
// Not all index pattern dependencies are avab. in jest context,
// prevent unrelated import errors by mocking kibana services
jest.mock('../kibana_services', () => {});
import { checkIndexPatternValid } from './indexing_service';
jest.mock('./kibana_services', () => {});
import { checkIndexPatternValid } from './validate_index_name';
describe('indexing_service', () => {
describe('checkIndexPatternValid', () => {
const validNames = [
'lowercaseletters', // Lowercase only
'123', // Cannot include \, /, *, ?, ", <, >, |, " " (space character), , (comma), #

View file

@ -0,0 +1,45 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { getIndexPatternService } from './kibana_services';
import { checkIndexExists } from './api';
export function checkIndexPatternValid(name: string) {
const byteLength = encodeURI(name).split(/%(?:u[0-9A-F]{2})?[0-9A-F]{2}|./).length - 1;
const reg = new RegExp('[\\\\/*?"<>|\\s,#]+');
const indexPatternInvalid =
byteLength > 255 || // name can't be greater than 255 bytes
name !== name.toLowerCase() || // name should be lowercase
name === '.' ||
name === '..' || // name can't be . or ..
name.match(/^[-_+]/) !== null || // name can't start with these chars
name.match(reg) !== null; // name can't contain these chars
return !indexPatternInvalid;
}
export const validateIndexName = async (indexName: string) => {
if (!checkIndexPatternValid(indexName)) {
return i18n.translate('xpack.fileUpload.indexNameContainsIllegalCharactersErrorMessage', {
defaultMessage: 'Index name contains illegal characters.',
});
}
const indexPatternNames = await getIndexPatternService().getTitles();
if (indexPatternNames.includes(indexName)) {
return i18n.translate('xpack.fileUpload.indexPatternAlreadyExistsErrorMessage', {
defaultMessage: 'Index pattern already exists.',
});
}
const indexExists = await checkIndexExists(indexName);
if (indexExists) {
return i18n.translate('xpack.fileUpload.indexNameAlreadyExistsErrorMessage', {
defaultMessage: 'Index name already exists.',
});
}
};

View file

@ -195,7 +195,7 @@ export function fileUploadRoutes(coreSetup: CoreSetup<StartDeps, unknown>, logge
body: schema.object({ index: schema.string() }),
},
options: {
tags: ['access:fileUpload:analyzeFile'],
tags: ['access:fileUpload:import'],
},
},
async (context, request, response) => {

View file

@ -26,14 +26,14 @@ export enum UPLOAD_STEPS {
}
enum INDEXING_STAGE {
READY = 'READY',
CONFIGURE = 'CONFIGURE',
TRIGGERED = 'TRIGGERED',
SUCCESS = 'SUCCESS',
ERROR = 'ERROR',
}
interface State {
indexingStage: INDEXING_STAGE | null;
indexingStage: INDEXING_STAGE;
fileUploadComponent: React.ComponentType<FileUploadComponentProps> | null;
results?: FileUploadGeoResults;
}
@ -42,7 +42,7 @@ export class ClientFileCreateSourceEditor extends Component<RenderWizardArgument
private _isMounted: boolean = false;
state: State = {
indexingStage: null,
indexingStage: INDEXING_STAGE.CONFIGURE,
fileUploadComponent: null,
};
@ -58,7 +58,7 @@ export class ClientFileCreateSourceEditor extends Component<RenderWizardArgument
componentDidUpdate() {
if (
this.props.currentStepId === UPLOAD_STEPS.UPLOAD &&
this.state.indexingStage === INDEXING_STAGE.READY
this.state.indexingStage === INDEXING_STAGE.CONFIGURE
) {
this.setState({ indexingStage: INDEXING_STAGE.TRIGGERED });
this.props.startStepLoading();
@ -156,19 +156,6 @@ export class ClientFileCreateSourceEditor extends Component<RenderWizardArgument
this.setState({ indexingStage: INDEXING_STAGE.ERROR });
};
// Called on file upload screen when UI state changes
_onIndexReady = (indexReady: boolean) => {
if (!this._isMounted) {
return;
}
this.setState({ indexingStage: indexReady ? INDEXING_STAGE.READY : null });
if (indexReady) {
this.props.enableNextBtn();
} else {
this.props.disableNextBtn();
}
};
render() {
if (!this.state.fileUploadComponent) {
return null;
@ -181,7 +168,8 @@ export class ClientFileCreateSourceEditor extends Component<RenderWizardArgument
isIndexingTriggered={this.state.indexingStage === INDEXING_STAGE.TRIGGERED}
onFileSelect={this._onFileSelect}
onFileClear={this._onFileClear}
onIndexReady={this._onIndexReady}
enableImportBtn={this.props.enableNextBtn}
disableImportBtn={this.props.disableNextBtn}
onUploadComplete={this._onUploadComplete}
onUploadError={this._onUploadError}
/>

View file

@ -14,10 +14,15 @@ export default function ({ getPageObjects, getService }) {
const FILE_LOAD_DIR = 'test_upload_files';
const DEFAULT_LOAD_FILE_NAME = 'point.json';
const security = getService('security');
const retry = getService('retry');
describe('GeoJSON import layer panel', () => {
before(async () => {
await security.testUser.setRoles(['global_maps_all', 'global_index_pattern_management_all']);
await security.testUser.setRoles([
'global_maps_all',
'geoall_data_writer',
'global_index_pattern_management_all',
]);
await PageObjects.maps.openNewMap();
});
@ -87,23 +92,23 @@ export default function ({ getPageObjects, getService }) {
});
it('should prevent import button from activating unless valid index name provided', async () => {
// Set index to invalid name
await PageObjects.maps.setIndexName('NoCapitalLetters');
// Check button
let importButtonActive = await PageObjects.maps.importFileButtonEnabled();
expect(importButtonActive).to.be(false);
await retry.try(async () => {
const importButtonActive = await PageObjects.maps.importFileButtonEnabled();
expect(importButtonActive).to.be(false);
});
// Set index to valid name
await PageObjects.maps.setIndexName('validindexname');
// Check button
importButtonActive = await PageObjects.maps.importFileButtonEnabled();
expect(importButtonActive).to.be(true);
await retry.try(async () => {
const importButtonActive = await PageObjects.maps.importFileButtonEnabled();
expect(importButtonActive).to.be(true);
});
// Set index back to invalid name
await PageObjects.maps.setIndexName('?noquestionmarks?');
// Check button
importButtonActive = await PageObjects.maps.importFileButtonEnabled();
expect(importButtonActive).to.be(false);
await retry.try(async () => {
const importButtonActive = await PageObjects.maps.importFileButtonEnabled();
expect(importButtonActive).to.be(false);
});
});
});
}

View file

@ -14,6 +14,7 @@ export default function ({ getService, getPageObjects }) {
const log = getService('log');
const security = getService('security');
const browser = getService('browser');
const retry = getService('retry');
const IMPORT_FILE_PREVIEW_NAME = 'Import File';
const FILE_LOAD_DIR = 'test_upload_files';
@ -32,6 +33,10 @@ export default function ({ getService, getPageObjects }) {
const indexName = uuid();
await PageObjects.maps.setIndexName(indexName);
await retry.try(async () => {
const importButtonActive = await PageObjects.maps.importFileButtonEnabled();
expect(importButtonActive).to.be(true);
});
await PageObjects.maps.clickImportFileButton();
return indexName;
}