[Maps][File upload] Make index name form a component for reuse outside of file upload (#97744)

This commit is contained in:
Aaron Caldwell 2021-04-26 15:24:19 -06:00 committed by GitHub
parent aa7650699f
commit 6472e1a4b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 201 additions and 246 deletions

View file

@ -8,11 +8,13 @@
import React from 'react';
import { FileUploadComponentProps, lazyLoadModules } from '../lazy_load_bundle';
import type { IImporter, ImportFactoryOptions } from '../importer';
import { IndexNameFormProps } from '../';
import type { HasImportPermission, FindFileStructureResponse } from '../../common';
import type { getMaxBytes, getMaxBytesFormatted } from '../importer/get_max_bytes';
export interface FileUploadStartApi {
getFileUploadComponent(): ReturnType<typeof getFileUploadComponent>;
getIndexNameFormComponent(): Promise<React.ComponentType<IndexNameFormProps>>;
importerFactory: typeof importerFactory;
getMaxBytes: typeof getMaxBytes;
getMaxBytesFormatted: typeof getMaxBytesFormatted;
@ -35,6 +37,13 @@ export async function getFileUploadComponent(): Promise<
return fileUploadModules.JsonUploadAndParse;
}
export async function getIndexNameFormComponent(): Promise<
React.ComponentType<IndexNameFormProps>
> {
const fileUploadModules = await lazyLoadModules();
return fileUploadModules.IndexNameForm;
}
export async function importerFactory(
format: string,
options: ImportFactoryOptions

View file

@ -6,16 +6,12 @@
*/
import React, { ChangeEvent, Component } from 'react';
import { EuiForm, EuiFormRow, EuiFieldText, EuiSelect, EuiCallOut, EuiSpacer } from '@elastic/eui';
import { EuiForm, EuiFormRow, EuiSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { GeoJsonFilePicker, OnFileSelectParameters } from './geojson_file_picker';
import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/public';
import {
getExistingIndexNames,
getExistingIndexPatternNames,
checkIndexPatternValid,
// @ts-expect-error
} from '../../util/indexing_service';
import { IndexNameForm } from './index_name_form';
import { validateIndexName } from '../../util/indexing_service';
const GEO_FIELD_TYPE_OPTIONS = [
{
@ -41,38 +37,15 @@ interface Props {
interface State {
hasFile: boolean;
isPointsOnly: boolean;
indexNames: string[];
}
export class GeoJsonUploadForm extends Component<Props, State> {
private _isMounted = false;
state: State = {
hasFile: false,
isPointsOnly: false,
indexNames: [],
};
async componentDidMount() {
this._isMounted = true;
this._loadIndexNames();
}
componentWillUnmount() {
this._isMounted = false;
}
_loadIndexNames = async () => {
const indexNameList = await getExistingIndexNames();
const indexPatternList = await getExistingIndexPatternNames();
if (this._isMounted) {
this.setState({
indexNames: [...indexNameList, ...indexPatternList],
});
}
};
_onFileSelect = (onFileSelectParameters: OnFileSelectParameters) => {
_onFileSelect = async (onFileSelectParameters: OnFileSelectParameters) => {
this.setState({
hasFile: true,
isPointsOnly: onFileSelectParameters.hasPoints && !onFileSelectParameters.hasShapes,
@ -80,7 +53,8 @@ export class GeoJsonUploadForm extends Component<Props, State> {
this.props.onFileSelect(onFileSelectParameters);
this._onIndexNameChange(onFileSelectParameters.indexName);
const indexNameError = await validateIndexName(onFileSelectParameters.indexName);
this.props.onIndexNameChange(onFileSelectParameters.indexName, indexNameError);
const geoFieldType =
onFileSelectParameters.hasPoints && !onFileSelectParameters.hasShapes
@ -97,7 +71,7 @@ export class GeoJsonUploadForm extends Component<Props, State> {
this.props.onFileClear();
this._onIndexNameChange('');
this.props.onIndexNameChange('');
};
_onGeoFieldTypeSelect = (event: ChangeEvent<HTMLSelectElement>) => {
@ -106,28 +80,6 @@ export class GeoJsonUploadForm extends Component<Props, State> {
);
};
_onIndexNameChange = (name: string) => {
let error: string | undefined;
if (this.state.indexNames.includes(name)) {
error = i18n.translate('xpack.fileUpload.indexSettings.indexNameAlreadyExistsErrorMessage', {
defaultMessage: 'Index name already exists.',
});
} else if (!checkIndexPatternValid(name)) {
error = i18n.translate(
'xpack.fileUpload.indexSettings.indexNameContainsIllegalCharactersErrorMessage',
{
defaultMessage: 'Index name contains illegal characters.',
}
);
}
this.props.onIndexNameChange(name, error);
};
_onIndexNameChangeEvent = (event: ChangeEvent<HTMLInputElement>) => {
this._onIndexNameChange(event.target.value);
};
_renderGeoFieldTypeSelect() {
return this.state.hasFile && this.state.isPointsOnly ? (
<EuiFormRow
@ -145,82 +97,18 @@ export class GeoJsonUploadForm extends Component<Props, State> {
) : null;
}
_renderIndexNameInput() {
const isInvalid = this.props.indexNameError !== undefined;
return this.state.hasFile ? (
<>
<EuiFormRow
label={i18n.translate('xpack.fileUpload.indexSettings.enterIndexNameLabel', {
defaultMessage: 'Index name',
})}
isInvalid={isInvalid}
error={isInvalid ? [this.props.indexNameError] : []}
>
<EuiFieldText
data-test-subj="fileUploadIndexNameInput"
value={this.props.indexName}
onChange={this._onIndexNameChangeEvent}
isInvalid={isInvalid}
aria-label={i18n.translate('xpack.fileUpload.indexNameReqField', {
defaultMessage: 'Index name, required field',
})}
/>
</EuiFormRow>
<EuiSpacer size="m" />
<EuiCallOut
title={i18n.translate('xpack.fileUpload.indexSettings.indexNameGuidelines', {
defaultMessage: 'Index name guidelines',
})}
size="s"
>
<ul style={{ marginBottom: 0 }}>
<li>
{i18n.translate('xpack.fileUpload.indexSettings.guidelines.mustBeNewIndex', {
defaultMessage: 'Must be a new index',
})}
</li>
<li>
{i18n.translate('xpack.fileUpload.indexSettings.guidelines.lowercaseOnly', {
defaultMessage: 'Lowercase only',
})}
</li>
<li>
{i18n.translate('xpack.fileUpload.indexSettings.guidelines.cannotInclude', {
defaultMessage:
'Cannot include \\\\, /, *, ?, ", <, >, |, \
" " (space character), , (comma), #',
})}
</li>
<li>
{i18n.translate('xpack.fileUpload.indexSettings.guidelines.cannotStartWith', {
defaultMessage: 'Cannot start with -, _, +',
})}
</li>
<li>
{i18n.translate('xpack.fileUpload.indexSettings.guidelines.cannotBe', {
defaultMessage: 'Cannot be . or ..',
})}
</li>
<li>
{i18n.translate('xpack.fileUpload.indexSettings.guidelines.length', {
defaultMessage:
'Cannot be longer than 255 bytes (note it is bytes, \
so multi-byte characters will count towards the 255 \
limit faster)',
})}
</li>
</ul>
</EuiCallOut>
</>
) : null;
}
render() {
return (
<EuiForm>
<GeoJsonFilePicker onSelect={this._onFileSelect} onClear={this._onFileClear} />
{this._renderGeoFieldTypeSelect()}
{this._renderIndexNameInput()}
{this.state.hasFile ? (
<IndexNameForm
indexName={this.props.indexName}
indexNameError={this.props.indexNameError}
onIndexNameChange={this.props.onIndexNameChange}
/>
) : null}
</EuiForm>
);
}

View file

@ -0,0 +1,96 @@
/*
* 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 React, { ChangeEvent, Component } from 'react';
import { EuiFormRow, EuiFieldText, EuiCallOut, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { validateIndexName } from '../../util/indexing_service';
export interface Props {
indexName: string;
indexNameError?: string;
onIndexNameChange: (name: string, error?: string) => void;
}
export class IndexNameForm extends Component<Props> {
_onIndexNameChange = async (event: ChangeEvent<HTMLInputElement>) => {
const indexName = event.target.value;
const indexNameError = await validateIndexName(indexName);
this.props.onIndexNameChange(indexName, indexNameError);
};
render() {
const errors = [...(this.props.indexNameError ? [this.props.indexNameError] : [])];
return (
<>
<EuiFormRow
label={i18n.translate('xpack.fileUpload.indexNameForm.enterIndexNameLabel', {
defaultMessage: 'Index name',
})}
isInvalid={!!errors.length}
error={errors}
>
<EuiFieldText
data-test-subj="fileUploadIndexNameInput"
value={this.props.indexName}
onChange={this._onIndexNameChange}
isInvalid={!!errors.length}
aria-label={i18n.translate('xpack.fileUpload.indexNameForm.indexNameReqField', {
defaultMessage: 'Index name, required field',
})}
/>
</EuiFormRow>
<EuiSpacer size="m" />
<EuiCallOut
title={i18n.translate('xpack.fileUpload.indexNameForm.indexNameGuidelines', {
defaultMessage: 'Index name guidelines',
})}
size="s"
>
<ul style={{ marginBottom: 0 }}>
<li>
{i18n.translate('xpack.fileUpload.indexNameForm.guidelines.mustBeNewIndex', {
defaultMessage: 'Must be a new index',
})}
</li>
<li>
{i18n.translate('xpack.fileUpload.indexNameForm.guidelines.lowercaseOnly', {
defaultMessage: 'Lowercase only',
})}
</li>
<li>
{i18n.translate('xpack.fileUpload.indexNameForm.guidelines.cannotInclude', {
defaultMessage:
'Cannot include \\\\, /, *, ?, ", <, >, |, \
" " (space character), , (comma), #',
})}
</li>
<li>
{i18n.translate('xpack.fileUpload.indexNameForm.guidelines.cannotStartWith', {
defaultMessage: 'Cannot start with -, _, +',
})}
</li>
<li>
{i18n.translate('xpack.fileUpload.indexNameForm.guidelines.cannotBe', {
defaultMessage: 'Cannot be . or ..',
})}
</li>
<li>
{i18n.translate('xpack.fileUpload.indexNameForm.guidelines.length', {
defaultMessage:
'Cannot be longer than 255 bytes (note it is bytes, \
so multi-byte characters will count towards the 255 \
limit faster)',
})}
</li>
</ul>
</EuiCallOut>
</>
);
}
}

View file

@ -13,5 +13,7 @@ export function plugin() {
export * from './importer/types';
export { Props as IndexNameFormProps } from './components/geojson_upload_form/index_name_form';
export { FileUploadPluginStart } from './plugin';
export { FileUploadComponentProps, FileUploadGeoResults } from './lazy_load_bundle';

View file

@ -11,6 +11,7 @@ import { HttpStart } from 'src/core/public';
import { IImporter, ImportFactoryOptions } from '../importer';
import { getHttp } from '../kibana_services';
import { ES_FIELD_TYPES } from '../../../../../src/plugins/data/public';
import { IndexNameFormProps } from '../';
export interface FileUploadGeoResults {
indexPatternId: string;
@ -32,6 +33,7 @@ let loadModulesPromise: Promise<LazyLoadedFileUploadModules>;
interface LazyLoadedFileUploadModules {
JsonUploadAndParse: React.ComponentType<FileUploadComponentProps>;
IndexNameForm: React.ComponentType<IndexNameFormProps>;
importerFactory: (format: string, options: ImportFactoryOptions) => IImporter | undefined;
getHttp: () => HttpStart;
}
@ -42,12 +44,13 @@ export async function lazyLoadModules(): Promise<LazyLoadedFileUploadModules> {
}
loadModulesPromise = new Promise(async (resolve) => {
const { JsonUploadAndParse, importerFactory } = await import('./lazy');
const { JsonUploadAndParse, importerFactory, IndexNameForm } = await import('./lazy');
resolve({
JsonUploadAndParse,
importerFactory,
getHttp,
IndexNameForm,
});
});
return loadModulesPromise;

View file

@ -6,4 +6,5 @@
*/
export { JsonUploadAndParse } from '../../components/json_upload_and_parse';
export { IndexNameForm } from '../../components/geojson_upload_form/index_name_form';
export { importerFactory } from '../../importer';

View file

@ -11,6 +11,7 @@ import {
getFileUploadComponent,
importerFactory,
hasImportPermission,
getIndexNameFormComponent,
checkIndexExists,
getTimeFieldRange,
analyzeFile,
@ -42,6 +43,7 @@ export class FileUploadPlugin
setStartServices(core, plugins);
return {
getFileUploadComponent,
getIndexNameFormComponent,
importerFactory,
getMaxBytes,
getMaxBytesFormatted,

View file

@ -1,52 +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 { i18n } from '@kbn/i18n';
import { getHttp } from '../kibana_services';
export async function http(options) {
if (!(options && options.url)) {
throw i18n.translate('xpack.fileUpload.httpService.noUrl', {
defaultMessage: 'No URL provided',
});
}
const url = options.url || '';
const headers = {
'Content-Type': 'application/json',
...options.headers,
};
const allHeaders = options.headers === undefined ? headers : { ...options.headers, ...headers };
const body = options.data === undefined ? null : JSON.stringify(options.data);
const payload = {
method: options.method || 'GET',
headers: allHeaders,
credentials: 'same-origin',
query: options.query,
};
if (body !== null) {
payload.body = body;
}
return await doFetch(url, payload);
}
async function doFetch(url, payload) {
try {
return await getHttp().fetch(url, payload);
} catch (err) {
return {
failures: [
i18n.translate('xpack.fileUpload.httpService.fetchError', {
defaultMessage: 'Error performing fetch: {error}',
values: { error: err.message },
}),
],
};
}
}

View file

@ -1,41 +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 { http as httpService } from './http_service';
import { getSavedObjectsClient } from '../kibana_services';
export const getExistingIndexNames = async () => {
const indexes = await httpService({
url: `/api/index_management/indices`,
method: 'GET',
});
return indexes ? indexes.map(({ name }) => name) : [];
};
export const getExistingIndexPatternNames = async () => {
const indexPatterns = await getSavedObjectsClient()
.find({
type: 'index-pattern',
fields: ['id', 'title', 'type', 'fields'],
perPage: 10000,
})
.then(({ savedObjects }) => savedObjects.map((savedObject) => savedObject.get('title')));
return indexPatterns ? indexPatterns.map(({ name }) => name) : [];
};
export function checkIndexPatternValid(name) {
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;
}

View file

@ -0,0 +1,73 @@
/*
* 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

@ -8117,20 +8117,7 @@
"xpack.features.ossFeatures.visualizeShortUrlSubFeatureName": "短い URL",
"xpack.features.savedObjectsManagementFeatureName": "保存されたオブジェクトの管理",
"xpack.features.visualizeFeatureName": "Visualizeライブラリ",
"xpack.fileUpload.httpService.fetchError": "フェッチ実行エラー:{error}",
"xpack.fileUpload.httpService.noUrl": "URLが指定されていません",
"xpack.fileUpload.indexNameReqField": "インデックス名、必須フィールド",
"xpack.fileUpload.indexSettings.enterIndexNameLabel": "インデックス名",
"xpack.fileUpload.indexSettings.enterIndexTypeLabel": "インデックスタイプ",
"xpack.fileUpload.indexSettings.guidelines.cannotBe": ".または..にすることはできません。",
"xpack.fileUpload.indexSettings.guidelines.cannotInclude": "\\\\、/、*、?、\"、&lt;、>、|、 \" \" (スペース文字) 、, (カンマ) 、#を使用することはできません。",
"xpack.fileUpload.indexSettings.guidelines.cannotStartWith": "-、_、+を先頭にすることはできません",
"xpack.fileUpload.indexSettings.guidelines.length": "256バイト以上にすることはできません (これはバイト数であるため、複数バイト文字では255文字の文字制限のカウントが速くなります) ",
"xpack.fileUpload.indexSettings.guidelines.lowercaseOnly": "小文字のみ",
"xpack.fileUpload.indexSettings.guidelines.mustBeNewIndex": "新しいインデックスを作成する必要があります",
"xpack.fileUpload.indexSettings.indexNameAlreadyExistsErrorMessage": "インデックス名またはパターンはすでに存在します。",
"xpack.fileUpload.indexSettings.indexNameContainsIllegalCharactersErrorMessage": "インデックス名に許可されていない文字が含まれています。",
"xpack.fileUpload.indexSettings.indexNameGuidelines": "インデックス名ガイドライン",
"xpack.fileUpload.jsonUploadAndParse.dataIndexingError": "データインデックスエラー",
"xpack.fileUpload.jsonUploadAndParse.indexPatternError": "インデックスパターンエラー",
"xpack.fleet.agentBulkActions.clearSelection": "選択した項目をクリア",

View file

@ -8190,20 +8190,7 @@
"xpack.features.ossFeatures.visualizeShortUrlSubFeatureName": "短 URL",
"xpack.features.savedObjectsManagementFeatureName": "已保存对象管理",
"xpack.features.visualizeFeatureName": "Visualize 库",
"xpack.fileUpload.httpService.fetchError": "执行提取时出错:{error}",
"xpack.fileUpload.httpService.noUrl": "未提供 URL",
"xpack.fileUpload.indexNameReqField": "索引名称,必填字段",
"xpack.fileUpload.indexSettings.enterIndexNameLabel": "索引名称",
"xpack.fileUpload.indexSettings.enterIndexTypeLabel": "索引类型",
"xpack.fileUpload.indexSettings.guidelines.cannotBe": "不能为 . 或 ..",
"xpack.fileUpload.indexSettings.guidelines.cannotInclude": "不能包含 \\\\、/、*、?、\"、&lt;、>、|、 “ ” (空格字符) 、, (逗号) 、#",
"xpack.fileUpload.indexSettings.guidelines.cannotStartWith": "不能以 -、_、+ 开头",
"xpack.fileUpload.indexSettings.guidelines.length": "不能长于 255 字节 (注意是字节, 因此多字节字符将更快达到 255 字节限制) ",
"xpack.fileUpload.indexSettings.guidelines.lowercaseOnly": "仅小写",
"xpack.fileUpload.indexSettings.guidelines.mustBeNewIndex": "必须是新索引",
"xpack.fileUpload.indexSettings.indexNameAlreadyExistsErrorMessage": "索引名称或模式已存在。",
"xpack.fileUpload.indexSettings.indexNameContainsIllegalCharactersErrorMessage": "索引名称包含非法字符。",
"xpack.fileUpload.indexSettings.indexNameGuidelines": "索引名称指引",
"xpack.fileUpload.jsonUploadAndParse.dataIndexingError": "数据索引错误",
"xpack.fileUpload.jsonUploadAndParse.indexPatternError": "索引模式错误",
"xpack.fleet.agentBulkActions.agentsSelected": "已选择 {count, plural, other {# 个代理}}",