Index pattern management UI -> TypeScript and New Platform Ready (edit_index_pattern) (#64184)

* draft

* Converted edit_index_pattern to React. Created 'tab' component.

* Some fixes

* returned state_container

* Fixed tests and translation

* Some refactoring

* Fixed tests

* rermove unused translations

* update snapshots

* Some refactoring

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: Matt Kime <matt@mattki.me>
This commit is contained in:
Uladzislau Lasitsa 2020-04-24 14:56:59 +03:00 committed by GitHub
parent 4eb971c8c3
commit 66074f9042
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 861 additions and 792 deletions

View file

@ -6,156 +6,7 @@
role="region"
aria-label="{{::'kbn.management.editIndexPattern.detailsAria' | i18n: { defaultMessage: 'Index pattern details' } }}"
>
<!-- Header -->
<div id="reactIndexHeader"></div>
<div class="euiSpacer euiSpacer--s"></div>
<p ng-if="::(indexPattern.timeFieldName || (indexPattern.tags && indexPattern.tags.length))">
<span ng-if="::indexPattern.timeFieldName">
<span class="euiBadge euiBadge--warning">
<span class="euiBadge__content">
<span class="euiBadge__text">
<span
i18n-id="kbn.management.editIndexPattern.timeFilterHeader"
i18n-default-message="Time Filter field name: {timeFieldName}"
i18n-values="{ timeFieldName: indexPattern.timeFieldName }">
</span>
</span>
</span>
</span>
</span>
<span ng-repeat="tag in indexPattern.tags">
<span class="euiBadge euiBadge--hollow">
<span class="euiBadge__content">
<span class="euiBadge__text">
{{tag.name}}
</span>
</span>
</span>
</span>
</p>
<div class="euiSpacer euiSpacer--m"></div>
<div class="euiText">
<p>
<span i18n-id="kbn.management.editIndexPattern.timeFilterLabel.timeFilterDetail"
i18n-default-message="This page lists every field in the {indexPatternTitle} index and the field's associated core type as recorded by Elasticsearch. To change a field type, use the Elasticsearch"
i18n-values="{ html_indexPatternTitle: '<strong>' + indexPattern.title + '</strong>' }"></span>
<a target="_blank" class="euiLink euiLink--primary" href="http://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html">
<span i18n-id="kbn.management.editIndexPattern.timeFilterLabel.mappingAPILink"
i18n-default-message="Mapping API"></span>
<i aria-hidden="true" class="fa-link fa"></i>
</a>
</p>
</div>
<div class="euiSpacer euiSpacer--m"></div>
<!-- Alerts -->
<div
ng-if="conflictFields.length"
class="kuiInfoPanel kuiInfoPanel--warning kuiVerticalRhythm"
>
<div class="kuiInfoPanelHeader">
<span class="kuiInfoPanelHeader__icon kuiIcon kuiIcon--warning fa-bolt"></span>
<span class="kuiInfoPanelHeader__title"
i18n-id="kbn.management.editIndexPattern.mappingConflictHeader"
i18n-default-message="Mapping conflict"></span>
</div>
<div class="kuiInfoPanelBody">
<div class="kuiInfoPanelBody__message">
<span i18n-id="kbn.management.editIndexPattern.mappingConflictLabel"
i18n-default-message="{conflictFieldsLength, plural, one {A field is} other {# fields are}} defined as several types (string, integer, etc) across the indices that match this pattern. You may still be able to use these conflict fields in parts of Kibana, but they will be unavailable for functions that require Kibana to know their type. Correcting this issue will require reindexing your data."
i18n-values="{ conflictFieldsLength: conflictFields.length }"></span>
</div>
</div>
</div>
<!-- Tabs -->
<div class="kuiTabs kuiVerticalRhythm">
<button
class="kuiTab"
ng-repeat="editSection in editSections"
ng-class="{ 'kuiTab-isSelected': getCurrentTab() === editSection.index }"
ng-click="setCurrentTab(editSection.index)"
data-test-subj="tab-{{ editSection.index }}"
>
{{ editSection.title }}
<span
data-test-subj="tab-count-{{ editSection.index }}"
aria-label="{{:: editSection.count + ' ' + editSection.title}}"
>
<span ng-if="editSection.count != editSection.totalCount">
({{ editSection.count }} / {{ editSection.totalCount }})
</span>
<span ng-if="editSection.count == editSection.totalCount">
({{ editSection.count }})
</span>
</span>
</button>
</div>
<!-- Field Filters -->
<form role="form" class="kuiFieldGroup kuiVerticalRhythm">
<div class="kuiFieldGroupSection kuiFieldGroupSection--wide">
<div class="kuiSearchInput">
<div class="kuiSearchInput__icon kuiIcon fa-search"></div>
<input
class="kuiSearchInput__input"
type="text"
aria-label="{{::'kbn.management.editIndexPattern.fields.filterAria' | i18n: {defaultMessage: 'Filter'} }}"
ng-model="fieldFilter"
placeholder="{{::'kbn.management.editIndexPattern.fields.filterPlaceholder' | i18n: {defaultMessage: 'Filter'} }}"
data-test-subj="indexPatternFieldFilter"
>
</div>
</div>
<div
class="kuiFieldGroupSection"
ng-if="getCurrentTab() == 'indexedFields' && indexedFieldTypes.length > 0"
>
<select
data-test-subj="indexedFieldTypeFilterDropdown"
class="kuiSelect"
ng-model="indexedFieldTypeFilter"
ng-change="changeFilter('indexedFieldTypeFilter', indexedFieldTypeFilter)"
ng-options="o for o in indexedFieldTypes"
>
<option value=""
i18n-id="kbn.management.editIndexPattern.fields.allTypesDropDown"
i18n-default-message="All field types"></option>
</select>
</div>
<div
class="kuiFieldGroupSection"
ng-if="getCurrentTab() == 'scriptedFields' && scriptedFieldLanguages.length > 0"
>
<select
data-test-subj="scriptedFieldLanguageFilterDropdown"
class="kuiSelect"
ng-model="scriptedFieldLanguageFilter"
ng-change="changeFilter('scriptedFieldLanguageFilter', scriptedFieldLanguageFilter)"
ng-options="o for o in scriptedFieldLanguages"
>
<option value=""
i18n-id="kbn.management.editIndexPattern.fields.allLangsDropDown"
i18n-default-message="All languages"></option>
</select>
</div>
</form>
<!-- Tab content -->
<div class="kuiVerticalRhythm">
<div id="reactIndexedFieldsTable"></div>
<div id="reactScriptedFieldsTable"></div>
<div id="reactSourceFiltersTable"></div>
</div>
<div id="reactEditIndexPattern"></div>
</div>
</div>
</kbn-management-app>

View file

@ -1,511 +0,0 @@
/*
* 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 _ from 'lodash';
import { HashRouter } from 'react-router-dom';
import { IndexHeader } from './index_header';
import { CreateEditField } from './create_edit_field';
import { docTitle } from 'ui/doc_title';
import { KbnUrlProvider } from 'ui/url';
import { IndicesEditSectionsProvider } from './edit_sections';
import { fatalError, toastNotifications } from 'ui/notify';
import { RegistryFieldFormatEditorsProvider } from 'ui/registry/field_format_editors';
import uiRoutes from 'ui/routes';
import { uiModules } from 'ui/modules';
import template from './edit_index_pattern.html';
import createEditFieldtemplate from './create_edit_field.html';
import { fieldWildcardMatcher } from '../../../../../../../../plugins/kibana_utils/public';
import { subscribeWithScope } from '../../../../../../../../plugins/kibana_legacy/public';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { SourceFiltersTable } from './source_filters_table';
import { IndexedFieldsTable } from './indexed_fields_table';
import { ScriptedFieldsTable } from './scripted_fields_table';
import { i18n } from '@kbn/i18n';
import { I18nContext } from 'ui/i18n';
import { npStart } from 'ui/new_platform';
import {
getEditBreadcrumbs,
getEditFieldBreadcrumbs,
getCreateFieldBreadcrumbs,
} from '../breadcrumbs';
import { TAB_INDEXED_FIELDS, TAB_SCRIPTED_FIELDS, TAB_SOURCE_FILTERS } from './constants';
import { createEditIndexPatternPageStateContainer } from './edit_index_pattern_state_container';
const REACT_SOURCE_FILTERS_DOM_ELEMENT_ID = 'reactSourceFiltersTable';
const REACT_INDEXED_FIELDS_DOM_ELEMENT_ID = 'reactIndexedFieldsTable';
const REACT_SCRIPTED_FIELDS_DOM_ELEMENT_ID = 'reactScriptedFieldsTable';
const REACT_INDEX_HEADER_DOM_ELEMENT_ID = 'reactIndexHeader';
const EDIT_FIELD_PATH = '/management/kibana/index_patterns/{{indexPattern.id}}/field/{{name}}';
function updateSourceFiltersTable($scope) {
$scope.$$postDigest(() => {
const node = document.getElementById(REACT_SOURCE_FILTERS_DOM_ELEMENT_ID);
if (!node) {
return;
}
render(
<I18nContext>
<SourceFiltersTable
indexPattern={$scope.indexPattern}
filterFilter={$scope.fieldFilter}
fieldWildcardMatcher={$scope.fieldWildcardMatcher}
onAddOrRemoveFilter={() => {
$scope.editSections = $scope.editSectionsProvider(
$scope.indexPattern,
$scope.fieldFilter,
$scope.indexPatternListProvider
);
$scope.refreshFilters();
$scope.$apply();
}}
/>
</I18nContext>,
node
);
});
}
function destroySourceFiltersTable() {
const node = document.getElementById(REACT_SOURCE_FILTERS_DOM_ELEMENT_ID);
node && unmountComponentAtNode(node);
}
function updateScriptedFieldsTable($scope) {
$scope.$$postDigest(() => {
const node = document.getElementById(REACT_SCRIPTED_FIELDS_DOM_ELEMENT_ID);
if (!node) {
return;
}
render(
<I18nContext>
<ScriptedFieldsTable
indexPattern={$scope.indexPattern}
fieldFilter={$scope.fieldFilter}
scriptedFieldLanguageFilter={$scope.scriptedFieldLanguageFilter}
helpers={{
redirectToRoute: field => {
$scope.kbnUrl.changePath(EDIT_FIELD_PATH, field);
$scope.$apply();
},
getRouteHref: (obj, route) => $scope.kbnUrl.getRouteHref(obj, route),
}}
onRemoveField={() => {
$scope.editSections = $scope.editSectionsProvider(
$scope.indexPattern,
$scope.fieldFilter,
$scope.indexPatternListProvider
);
$scope.refreshFilters();
$scope.$apply();
}}
/>
</I18nContext>,
node
);
});
}
function destroyScriptedFieldsTable() {
const node = document.getElementById(REACT_SCRIPTED_FIELDS_DOM_ELEMENT_ID);
node && unmountComponentAtNode(node);
}
function updateIndexedFieldsTable($scope) {
$scope.$$postDigest(() => {
const node = document.getElementById(REACT_INDEXED_FIELDS_DOM_ELEMENT_ID);
if (!node) {
return;
}
render(
<I18nContext>
<IndexedFieldsTable
fields={$scope.fields}
indexPattern={$scope.indexPattern}
fieldFilter={$scope.fieldFilter}
fieldWildcardMatcher={$scope.fieldWildcardMatcher}
indexedFieldTypeFilter={$scope.indexedFieldTypeFilter}
helpers={{
redirectToRoute: field => {
$scope.kbnUrl.changePath(EDIT_FIELD_PATH, field);
$scope.$apply();
},
getFieldInfo: $scope.getFieldInfo,
}}
/>
</I18nContext>,
node
);
});
}
function destroyIndexedFieldsTable() {
const node = document.getElementById(REACT_INDEXED_FIELDS_DOM_ELEMENT_ID);
node && unmountComponentAtNode(node);
}
function destroyIndexHeader() {
const node = document.getElementById(REACT_INDEX_HEADER_DOM_ELEMENT_ID);
node && unmountComponentAtNode(node);
}
function renderIndexHeader($scope, config) {
$scope.$$postDigest(() => {
const node = document.getElementById(REACT_INDEX_HEADER_DOM_ELEMENT_ID);
if (!node) {
return;
}
render(
<I18nContext>
<IndexHeader
indexPattern={$scope.indexPattern}
setDefault={$scope.setDefaultPattern}
refreshFields={$scope.refreshFields}
deleteIndexPattern={$scope.removePattern}
defaultIndex={config.get('defaultIndex')}
/>
</I18nContext>,
node
);
});
}
function handleTabChange($scope, newTab) {
destroyIndexedFieldsTable();
destroySourceFiltersTable();
destroyScriptedFieldsTable();
updateTables($scope, newTab);
}
function updateTables($scope, currentTab) {
switch (currentTab) {
case TAB_SCRIPTED_FIELDS:
return updateScriptedFieldsTable($scope);
case TAB_INDEXED_FIELDS:
return updateIndexedFieldsTable($scope);
case TAB_SOURCE_FILTERS:
return updateSourceFiltersTable($scope);
}
}
uiRoutes.when('/management/kibana/index_patterns/:indexPatternId', {
template,
k7Breadcrumbs: getEditBreadcrumbs,
resolve: {
indexPattern: function($route, Promise, redirectWhenMissing) {
const { indexPatterns } = npStart.plugins.data;
return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)).catch(
redirectWhenMissing('/management/kibana/index_patterns')
);
},
},
});
uiModules
.get('apps/management')
.controller('managementIndexPatternsEdit', function(
$scope,
$location,
$route,
Promise,
config,
Private
) {
const {
startSyncingState,
stopSyncingState,
setCurrentTab,
getCurrentTab,
state$,
} = createEditIndexPatternPageStateContainer({
useHashedUrl: config.get('state:storeInSessionStorage'),
defaultTab: TAB_INDEXED_FIELDS,
});
$scope.getCurrentTab = getCurrentTab;
$scope.setCurrentTab = setCurrentTab;
const stateChangedSub = subscribeWithScope(
$scope,
state$,
{
next: ({ tab }) => {
handleTabChange($scope, tab);
},
},
fatalError
);
handleTabChange($scope, getCurrentTab()); // setup initial tab depending on initial tab state
startSyncingState(); // starts syncing state between state container and url
const destroyState = () => {
stateChangedSub.unsubscribe();
stopSyncingState();
};
$scope.fieldWildcardMatcher = (...args) =>
fieldWildcardMatcher(...args, config.get('metaFields'));
$scope.editSectionsProvider = Private(IndicesEditSectionsProvider);
$scope.kbnUrl = Private(KbnUrlProvider);
$scope.indexPattern = $route.current.locals.indexPattern;
$scope.indexPatternListProvider = npStart.plugins.indexPatternManagement.list;
$scope.indexPattern.tags = npStart.plugins.indexPatternManagement.list.getIndexPatternTags(
$scope.indexPattern,
$scope.indexPattern.id === config.get('defaultIndex')
);
$scope.getFieldInfo = npStart.plugins.indexPatternManagement.list.getFieldInfo;
docTitle.change($scope.indexPattern.title);
const otherPatterns = _.filter($route.current.locals.indexPatterns, pattern => {
return pattern.id !== $scope.indexPattern.id;
});
$scope.$watch('indexPattern.fields', function() {
$scope.editSections = $scope.editSectionsProvider(
$scope.indexPattern,
$scope.fieldFilter,
npStart.plugins.indexPatternManagement.list
);
$scope.refreshFilters();
$scope.fields = $scope.indexPattern.getNonScriptedFields();
});
$scope.refreshFilters = function() {
const indexedFieldTypes = [];
const scriptedFieldLanguages = [];
$scope.indexPattern.fields.forEach(field => {
if (field.scripted) {
scriptedFieldLanguages.push(field.lang);
} else {
indexedFieldTypes.push(field.type);
}
});
$scope.indexedFieldTypes = _.unique(indexedFieldTypes);
$scope.scriptedFieldLanguages = _.unique(scriptedFieldLanguages);
};
$scope.changeFilter = function(filter, val) {
$scope[filter] = val || ''; // null causes filter to check for null explicitly
};
$scope.$watchCollection('indexPattern.fields', function() {
$scope.conflictFields = $scope.indexPattern.fields.filter(field => field.type === 'conflict');
});
$scope.refreshFields = function() {
const confirmMessage = i18n.translate('kbn.management.editIndexPattern.refreshLabel', {
defaultMessage: 'This action resets the popularity counter of each field.',
});
const confirmModalOptions = {
confirmButtonText: i18n.translate('kbn.management.editIndexPattern.refreshButton', {
defaultMessage: 'Refresh',
}),
title: i18n.translate('kbn.management.editIndexPattern.refreshHeader', {
defaultMessage: 'Refresh field list?',
}),
};
npStart.core.overlays
.openConfirm(confirmMessage, confirmModalOptions)
.then(async isConfirmed => {
if (isConfirmed) {
await $scope.indexPattern.init(true);
$scope.fields = $scope.indexPattern.getNonScriptedFields();
}
});
};
$scope.removePattern = function() {
function doRemove() {
if ($scope.indexPattern.id === config.get('defaultIndex')) {
config.remove('defaultIndex');
if (otherPatterns.length) {
config.set('defaultIndex', otherPatterns[0].id);
}
}
Promise.resolve($scope.indexPattern.destroy())
.then(function() {
$location.url('/management/kibana/index_patterns');
})
.catch(fatalError);
}
const confirmModalOptions = {
confirmButtonText: i18n.translate('kbn.management.editIndexPattern.deleteButton', {
defaultMessage: 'Delete',
}),
title: i18n.translate('kbn.management.editIndexPattern.deleteHeader', {
defaultMessage: 'Delete index pattern?',
}),
};
npStart.core.overlays.openConfirm('', confirmModalOptions).then(isConfirmed => {
if (isConfirmed) {
doRemove();
}
});
};
$scope.setDefaultPattern = function() {
config.set('defaultIndex', $scope.indexPattern.id);
};
$scope.setIndexPatternsTimeField = function(field) {
if (field.type !== 'date') {
const errorMessage = i18n.translate('kbn.management.editIndexPattern.notDateErrorMessage', {
defaultMessage: 'That field is a {fieldType} not a date.',
values: { fieldType: field.type },
});
toastNotifications.addDanger(errorMessage);
return;
}
$scope.indexPattern.timeFieldName = field.name;
return $scope.indexPattern.save();
};
$scope.$watch('fieldFilter', () => {
$scope.editSections = $scope.editSectionsProvider(
$scope.indexPattern,
$scope.fieldFilter,
npStart.plugins.indexPatternManagement.list
);
if ($scope.fieldFilter === undefined) {
return;
}
updateTables($scope, getCurrentTab());
});
$scope.$watch('indexedFieldTypeFilter', () => {
if ($scope.indexedFieldTypeFilter !== undefined && getCurrentTab() === TAB_INDEXED_FIELDS) {
updateIndexedFieldsTable($scope);
}
});
$scope.$watch('scriptedFieldLanguageFilter', () => {
if (
$scope.scriptedFieldLanguageFilter !== undefined &&
getCurrentTab() === TAB_SCRIPTED_FIELDS
) {
updateScriptedFieldsTable($scope);
}
});
$scope.$on('$destroy', () => {
destroyIndexedFieldsTable();
destroyScriptedFieldsTable();
destroySourceFiltersTable();
destroyIndexHeader();
destroyState();
});
renderIndexHeader($scope, config);
});
// routes for create edit field. Will be removed after migartion all component to react.
const REACT_FIELD_EDITOR_ID = 'reactFieldEditor';
const renderCreateEditField = ($scope, $route, getConfig, $http, fieldFormatEditors) => {
$scope.$$postDigest(() => {
const node = document.getElementById(REACT_FIELD_EDITOR_ID);
if (!node) {
return;
}
render(
<HashRouter>
<I18nContext>
<CreateEditField
indexPattern={$route.current.locals.indexPattern}
mode={$route.current.mode}
fieldName={$route.current.params.fieldName}
fieldFormatEditors={fieldFormatEditors}
getConfig={getConfig}
services={{
http: $http,
notifications: npStart.core.notifications,
docTitle: npStart.core.chrome.docTitle,
}}
/>
</I18nContext>
</HashRouter>,
node
);
});
};
const destroyCreateEditField = () => {
const node = document.getElementById(REACT_FIELD_EDITOR_ID);
node && unmountComponentAtNode(node);
};
uiRoutes
.when('/management/kibana/index_patterns/:indexPatternId/field/:fieldName*', {
mode: 'edit',
k7Breadcrumbs: getEditFieldBreadcrumbs,
})
.when('/management/kibana/index_patterns/:indexPatternId/create-field/', {
mode: 'create',
k7Breadcrumbs: getCreateFieldBreadcrumbs,
})
.defaults(/management\/kibana\/index_patterns\/[^\/]+\/(field|create-field)(\/|$)/, {
template: createEditFieldtemplate,
mapBreadcrumbs($route, breadcrumbs) {
const { indexPattern } = $route.current.locals;
return breadcrumbs.map(crumb => {
if (crumb.id !== indexPattern.id) {
return crumb;
}
return {
...crumb,
display: indexPattern.title,
};
});
},
resolve: {
indexPattern: function($route, Promise, redirectWhenMissing) {
const { indexPatterns } = npStart.plugins.data;
return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)).catch(
redirectWhenMissing('/management/kibana/index_patterns')
);
},
},
controllerAs: 'fieldSettings',
controller: function FieldEditorPageController($scope, $route, $http, Private, config) {
const getConfig = (...args) => config.get(...args);
const fieldFormatEditors = Private(RegistryFieldFormatEditorsProvider);
renderCreateEditField($scope, $route, getConfig, $http, fieldFormatEditors);
$scope.$on('$destroy', () => {
destroyCreateEditField();
});
},
});

View file

@ -0,0 +1,238 @@
/*
* 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 { filter } from 'lodash';
import React, { useEffect, useState, useCallback } from 'react';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiBadge,
EuiText,
EuiLink,
EuiIcon,
EuiCallOut,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { IndexPattern, IndexPatternField } from '../../../../../../../../plugins/data/public';
import {
ChromeDocTitle,
NotificationsStart,
OverlayStart,
} from '../../../../../../../../core/public';
import { IndexPatternManagementStart } from '../../../../../../../../plugins/index_pattern_management/public';
import { Tabs } from './tabs';
import { IndexHeader } from './index_header';
interface EditIndexPatternProps extends RouteComponentProps {
indexPattern: IndexPattern;
indexPatterns: IndexPattern[];
config: Record<string, any>;
services: {
notifications: NotificationsStart;
docTitle: ChromeDocTitle;
overlays: OverlayStart;
indexPatternManagement: IndexPatternManagementStart;
};
}
const mappingAPILink = i18n.translate(
'kbn.management.editIndexPattern.timeFilterLabel.mappingAPILink',
{
defaultMessage: 'Mapping API',
}
);
const mappingConflictHeader = i18n.translate(
'kbn.management.editIndexPattern.mappingConflictHeader',
{
defaultMessage: 'Mapping conflict',
}
);
const confirmMessage = i18n.translate('kbn.management.editIndexPattern.refreshLabel', {
defaultMessage: 'This action resets the popularity counter of each field.',
});
const confirmModalOptionsRefresh = {
confirmButtonText: i18n.translate('kbn.management.editIndexPattern.refreshButton', {
defaultMessage: 'Refresh',
}),
title: i18n.translate('kbn.management.editIndexPattern.refreshHeader', {
defaultMessage: 'Refresh field list?',
}),
};
const confirmModalOptionsDelete = {
confirmButtonText: i18n.translate('kbn.management.editIndexPattern.deleteButton', {
defaultMessage: 'Delete',
}),
title: i18n.translate('kbn.management.editIndexPattern.deleteHeader', {
defaultMessage: 'Delete index pattern?',
}),
};
export const EditIndexPattern = withRouter(
({ indexPattern, indexPatterns, config, services, history, location }: EditIndexPatternProps) => {
const [fields, setFields] = useState<IndexPatternField[]>(indexPattern.getNonScriptedFields());
const [conflictedFields, setConflictedFields] = useState<IndexPatternField[]>(
indexPattern.fields.filter(field => field.type === 'conflict')
);
const [defaultIndex, setDefaultIndex] = useState<string>(config.get('defaultIndex'));
const [tags, setTags] = useState<any[]>([]);
useEffect(() => {
setFields(indexPattern.getNonScriptedFields());
setConflictedFields(indexPattern.fields.filter(field => field.type === 'conflict'));
}, [indexPattern, indexPattern.fields]);
useEffect(() => {
const indexPatternTags =
services.indexPatternManagement.list.getIndexPatternTags(
indexPattern,
indexPattern.id === defaultIndex
) || [];
setTags(indexPatternTags);
}, [defaultIndex, indexPattern, services.indexPatternManagement.list]);
const setDefaultPattern = useCallback(() => {
config.set('defaultIndex', indexPattern.id);
setDefaultIndex(indexPattern.id || '');
}, [config, indexPattern.id]);
const refreshFields = () => {
services.overlays
.openConfirm(confirmMessage, confirmModalOptionsRefresh)
.then(async isConfirmed => {
if (isConfirmed) {
await indexPattern.init(true);
setFields(indexPattern.getNonScriptedFields());
}
});
};
const removePattern = () => {
function doRemove() {
if (indexPattern.id === defaultIndex) {
config.remove('defaultIndex');
const otherPatterns = filter(indexPatterns, pattern => {
return pattern.id !== indexPattern.id;
});
if (otherPatterns.length) {
config.set('defaultIndex', otherPatterns[0].id);
}
}
Promise.resolve(indexPattern.destroy()).then(function() {
history.push('/management/kibana/index_patterns');
});
}
services.overlays.openConfirm('', confirmModalOptionsDelete).then(isConfirmed => {
if (isConfirmed) {
doRemove();
}
});
};
const timeFilterHeader = i18n.translate('kbn.management.editIndexPattern.timeFilterHeader', {
defaultMessage: "Time Filter field name: '{timeFieldName}'",
values: { timeFieldName: indexPattern.timeFieldName },
});
const mappingConflictLabel = i18n.translate(
'kbn.management.editIndexPattern.mappingConflictLabel',
{
defaultMessage:
'{conflictFieldsLength, plural, one {A field is} other {# fields are}} defined as several types (string, integer, etc) across the indices that match this pattern. You may still be able to use these conflict fields in parts of Kibana, but they will be unavailable for functions that require Kibana to know their type. Correcting this issue will require reindexing your data.',
values: { conflictFieldsLength: conflictedFields.length },
}
);
services.docTitle.change(indexPattern.title);
const showTagsSection = Boolean(indexPattern.timeFieldName || (tags && tags.length > 0));
return (
<EuiFlexGroup direction="column">
<EuiFlexItem>
<IndexHeader
indexPattern={indexPattern}
setDefault={setDefaultPattern}
refreshFields={refreshFields}
deleteIndexPattern={removePattern}
defaultIndex={defaultIndex}
/>
<EuiSpacer size="s" />
{showTagsSection && (
<EuiFlexGroup wrap>
{Boolean(indexPattern.timeFieldName) && (
<EuiFlexItem grow={false}>
<EuiBadge color="warning">{timeFilterHeader}</EuiBadge>
</EuiFlexItem>
)}
{tags.map((tag: any) => (
<EuiFlexItem grow={false} key={tag.key}>
<EuiBadge color="hollow">{tag.name}</EuiBadge>
</EuiFlexItem>
))}
</EuiFlexGroup>
)}
<EuiSpacer size="m" />
<EuiText>
<p>
<FormattedMessage
id="kbn.management.editIndexPattern.timeFilterLabel.timeFilterDetail"
defaultMessage="This page lists every field in the {indexPatternTitle} index and the field's associated core type as recorded by Elasticsearch. To change a field type, use the Elasticsearch"
values={{ indexPatternTitle: <strong>{indexPattern.title}</strong> }}
/>{' '}
<EuiLink
href="http://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html"
target="_blank"
>
{mappingAPILink}
<EuiIcon type="link" />
</EuiLink>
</p>
</EuiText>
{conflictedFields.length > 0 && (
<EuiCallOut title={mappingConflictHeader} color="warning" iconType="alert">
<p>{mappingConflictLabel}</p>
</EuiCallOut>
)}
</EuiFlexItem>
<EuiFlexItem>
<Tabs
indexPattern={indexPattern}
fields={fields}
config={config}
services={{
indexPatternManagement: services.indexPatternManagement,
}}
history={history}
location={location}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}
);

View file

@ -25,7 +25,7 @@ import {
} from '../../../../../../../../plugins/kibana_utils/public';
interface IEditIndexPatternState {
tab: string; // TODO: type those 3 tabs with enum, when edit_index_pattern.js migrated to ts
tab: string;
}
/**
@ -38,7 +38,6 @@ export function createEditIndexPatternPageStateContainer({
defaultTab: string;
useHashedUrl: boolean;
}) {
// until angular is used as shell - use hash history
const history = createHashHistory();
// query param to store app state at
const stateStorageKey = '_a';
@ -78,12 +77,10 @@ export function createEditIndexPatternPageStateContainer({
// makes sure initial url is the same as initial state (this is not really required)
kbnUrlStateStorage.set(stateStorageKey, stateContainer.getState(), { replace: true });
// expose api needed for Controller
return {
startSyncingState: start,
stopSyncingState: stop,
setCurrentTab: (newTab: string) => stateContainer.transitions.setTab(newTab),
getCurrentTab: () => stateContainer.selectors.tab(),
state$: stateContainer.state$,
};
}

View file

@ -1,80 +0,0 @@
/*
* 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 _ from 'lodash';
import { i18n } from '@kbn/i18n';
function filterBy(items, key, filter) {
const lowercaseFilter = (filter || '').toLowerCase();
return items.filter(item => item[key].toLowerCase().includes(lowercaseFilter));
}
function getCounts(fields, sourceFilters, fieldFilter = '') {
const fieldCount = _.countBy(filterBy(fields, 'name', fieldFilter), function(field) {
return field.scripted ? 'scripted' : 'indexed';
});
_.defaults(fieldCount, {
indexed: 0,
scripted: 0,
sourceFilters: sourceFilters ? filterBy(sourceFilters, 'value', fieldFilter).length : 0,
});
return fieldCount;
}
export function IndicesEditSectionsProvider() {
return function(indexPattern, fieldFilter, indexPatternListProvider) {
const totalCount = getCounts(indexPattern.fields, indexPattern.sourceFilters);
const filteredCount = getCounts(indexPattern.fields, indexPattern.sourceFilters, fieldFilter);
const editSections = [];
editSections.push({
title: i18n.translate('kbn.management.editIndexPattern.tabs.fieldsHeader', {
defaultMessage: 'Fields',
}),
index: 'indexedFields',
count: filteredCount.indexed,
totalCount: totalCount.indexed,
});
if (indexPatternListProvider.areScriptedFieldsEnabled(indexPattern)) {
editSections.push({
title: i18n.translate('kbn.management.editIndexPattern.tabs.scriptedHeader', {
defaultMessage: 'Scripted fields',
}),
index: 'scriptedFields',
count: filteredCount.scripted,
totalCount: totalCount.scripted,
});
}
editSections.push({
title: i18n.translate('kbn.management.editIndexPattern.tabs.sourceHeader', {
defaultMessage: 'Source filters',
}),
index: 'sourceFilters',
count: filteredCount.sourceFilters,
totalCount: totalCount.sourceFilters,
});
return editSections;
};
}

View file

@ -1,19 +0,0 @@
<div class="actions">
<a
data-test-subj="indexPatternFieldEditButton"
ng-href="{{ kbnUrl.getRouteHref(field, 'edit') }}"
aria-label="{{::'kbn.management.editIndexPattern.editFieldButton' | i18n: { defaultMessage: 'Edit' } }}"
class="kuiButton kuiButton--basic kuiButton--small"
>
<span aria-hidden="true" class="kuiIcon fa-pencil"></span>
</a>
<button
ng-if="field.scripted"
ng-click="remove(field)"
class="kuiButton kuiButton--danger kuiButton--small"
aria-label="{{::'kbn.management.editIndexPattern.deleteFieldButton' | i18n: { defaultMessage: 'Delete' } }}"
>
<span aria-hidden="true" class="kuiIcon fa-trash"></span>
</button>
</div>

View file

@ -17,4 +17,159 @@
* under the License.
*/
import './edit_index_pattern';
import React from 'react';
import { HashRouter } from 'react-router-dom';
import { render, unmountComponentAtNode } from 'react-dom';
import { RegistryFieldFormatEditorsProvider } from 'ui/registry/field_format_editors';
import uiRoutes from 'ui/routes';
import { uiModules } from 'ui/modules';
import { I18nContext } from 'ui/i18n';
import { npStart } from 'ui/new_platform';
import template from './edit_index_pattern.html';
import createEditFieldtemplate from './create_edit_field.html';
import {
getEditBreadcrumbs,
getEditFieldBreadcrumbs,
getCreateFieldBreadcrumbs,
} from '../breadcrumbs';
import { EditIndexPattern } from './edit_index_pattern';
import { CreateEditField } from './create_edit_field';
const REACT_EDIT_INDEX_PATTERN_DOM_ELEMENT_ID = 'reactEditIndexPattern';
function destroyEditIndexPattern() {
const node = document.getElementById(REACT_EDIT_INDEX_PATTERN_DOM_ELEMENT_ID);
node && unmountComponentAtNode(node);
}
function renderEditIndexPattern($scope, config, $route) {
$scope.$$postDigest(() => {
const node = document.getElementById(REACT_EDIT_INDEX_PATTERN_DOM_ELEMENT_ID);
if (!node) {
return;
}
render(
<HashRouter>
<I18nContext>
<EditIndexPattern
indexPattern={$route.current.locals.indexPattern}
indexPatterns={$route.current.locals.indexPatterns}
config={config}
services={{
notifications: npStart.core.notifications,
docTitle: npStart.core.chrome.docTitle,
overlays: npStart.core.overlays,
indexPatternManagement: npStart.plugins.indexPatternManagement,
}}
/>
</I18nContext>
</HashRouter>,
node
);
});
}
uiRoutes.when('/management/kibana/index_patterns/:indexPatternId', {
template,
k7Breadcrumbs: getEditBreadcrumbs,
resolve: {
indexPattern: function($route, Promise, redirectWhenMissing) {
const { indexPatterns } = npStart.plugins.data;
return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)).catch(
redirectWhenMissing('/management/kibana/index_patterns')
);
},
},
});
uiModules
.get('apps/management')
.controller('managementIndexPatternsEdit', function($scope, $route, config) {
$scope.$on('$destroy', () => {
destroyEditIndexPattern();
});
renderEditIndexPattern($scope, config, $route);
});
// routes for create edit field. Will be removed after migartion all component to react.
const REACT_FIELD_EDITOR_ID = 'reactFieldEditor';
const renderCreateEditField = ($scope, $route, getConfig, $http, fieldFormatEditors) => {
$scope.$$postDigest(() => {
const node = document.getElementById(REACT_FIELD_EDITOR_ID);
if (!node) {
return;
}
render(
<HashRouter>
<I18nContext>
<CreateEditField
indexPattern={$route.current.locals.indexPattern}
mode={$route.current.mode}
fieldName={$route.current.params.fieldName}
fieldFormatEditors={fieldFormatEditors}
getConfig={getConfig}
services={{
http: $http,
notifications: npStart.core.notifications,
docTitle: npStart.core.chrome.docTitle,
}}
/>
</I18nContext>
</HashRouter>,
node
);
});
};
const destroyCreateEditField = () => {
const node = document.getElementById(REACT_FIELD_EDITOR_ID);
node && unmountComponentAtNode(node);
};
uiRoutes
.when('/management/kibana/index_patterns/:indexPatternId/field/:fieldName*', {
mode: 'edit',
k7Breadcrumbs: getEditFieldBreadcrumbs,
})
.when('/management/kibana/index_patterns/:indexPatternId/create-field/', {
mode: 'create',
k7Breadcrumbs: getCreateFieldBreadcrumbs,
})
.defaults(/management\/kibana\/index_patterns\/[^\/]+\/(field|create-field)(\/|$)/, {
template: createEditFieldtemplate,
mapBreadcrumbs($route, breadcrumbs) {
const { indexPattern } = $route.current.locals;
return breadcrumbs.map(crumb => {
if (crumb.id !== indexPattern.id) {
return crumb;
}
return {
...crumb,
display: indexPattern.title,
};
});
},
resolve: {
indexPattern: function($route, Promise, redirectWhenMissing) {
const { indexPatterns } = npStart.plugins.data;
return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)).catch(
redirectWhenMissing('/management/kibana/index_patterns')
);
},
},
controllerAs: 'fieldSettings',
controller: function FieldEditorPageController($scope, $route, $http, Private, config) {
const getConfig = (...args) => config.get(...args);
const fieldFormatEditors = Private(RegistryFieldFormatEditorsProvider);
renderCreateEditField($scope, $route, getConfig, $http, fieldFormatEditors);
$scope.$on('$destroy', () => {
destroyCreateEditField();
});
},
});

View file

@ -92,7 +92,7 @@ export function IndexHeader({
<EuiButtonIcon
color="text"
onClick={setDefault}
iconType="starFilledSpace"
iconType="starFilled"
aria-label={setDefaultAriaLabel}
data-test-subj="setDefaultIndexPatternButton"
/>

View file

@ -19,7 +19,11 @@
import React, { Component } from 'react';
import { createSelector } from 'reselect';
import { IndexPatternField, IIndexPattern } from '../../../../../../../../../plugins/data/public';
import {
IndexPatternField,
IIndexPattern,
IFieldType,
} from '../../../../../../../../../plugins/data/public';
import { Table } from './components/table';
import { getFieldFormat } from './lib';
import { IndexedFieldItem } from './types';
@ -31,7 +35,7 @@ interface IndexedFieldsTableProps {
indexedFieldTypeFilter?: string;
helpers: {
redirectToRoute: (obj: any) => void;
getFieldInfo: (indexPattern: IIndexPattern, field: string) => string[];
getFieldInfo: (indexPattern: IIndexPattern, field: IFieldType) => string[];
};
fieldWildcardMatcher: (filters: any[]) => (val: any) => boolean;
}
@ -76,7 +80,7 @@ export class IndexedFieldsTable extends Component<
indexPattern: field.indexPattern,
format: getFieldFormat(indexPattern, field.name),
excluded: fieldWildcardMatch ? fieldWildcardMatch(field.name) : false,
info: helpers.getFieldInfo && helpers.getFieldInfo(indexPattern, field.name),
info: helpers.getFieldInfo && helpers.getFieldInfo(indexPattern, field),
};
})) ||
[]

View file

@ -3,7 +3,7 @@
exports[`ScriptedFieldsTable should filter based on the lang filter 1`] = `
<Fragment>
<header
addScriptedFieldUrl="#"
addScriptedFieldUrl="http://localhost/#/management/kibana/index_patterns/undefined/create-field/"
/>
<call-outs
deprecatedLangsInUse={
@ -45,7 +45,7 @@ exports[`ScriptedFieldsTable should filter based on the lang filter 1`] = `
exports[`ScriptedFieldsTable should filter based on the query bar 1`] = `
<Fragment>
<header
addScriptedFieldUrl="#"
addScriptedFieldUrl="http://localhost/#/management/kibana/index_patterns/undefined/create-field/"
/>
<call-outs
deprecatedLangsInUse={Array []}
@ -78,7 +78,7 @@ exports[`ScriptedFieldsTable should filter based on the query bar 1`] = `
exports[`ScriptedFieldsTable should hide the table if there are no scripted fields 1`] = `
<Fragment>
<header
addScriptedFieldUrl="#"
addScriptedFieldUrl="http://localhost/#/management/kibana/index_patterns/undefined/create-field/"
/>
<call-outs
deprecatedLangsInUse={Array []}
@ -103,7 +103,7 @@ exports[`ScriptedFieldsTable should hide the table if there are no scripted fiel
exports[`ScriptedFieldsTable should render normally 1`] = `
<Fragment>
<header
addScriptedFieldUrl="#"
addScriptedFieldUrl="http://localhost/#/management/kibana/index_patterns/undefined/create-field/"
/>
<call-outs
deprecatedLangsInUse={Array []}
@ -141,7 +141,7 @@ exports[`ScriptedFieldsTable should render normally 1`] = `
exports[`ScriptedFieldsTable should show a delete modal 1`] = `
<Fragment>
<header
addScriptedFieldUrl="#"
addScriptedFieldUrl="http://localhost/#/management/kibana/index_patterns/undefined/create-field/"
/>
<call-outs
deprecatedLangsInUse={Array []}

View file

@ -37,7 +37,7 @@ interface ScriptedFieldsTableProps {
scriptedFieldLanguageFilter?: string;
helpers: {
redirectToRoute: Function;
getRouteHref: Function;
getRouteHref?: Function;
};
onRemoveField?: () => void;
}
@ -136,14 +136,19 @@ export class ScriptedFieldsTable extends Component<
};
render() {
const { helpers, indexPattern } = this.props;
const { indexPattern } = this.props;
const { fieldToDelete, deprecatedLangsInUse } = this.state;
const items = this.getFilteredItems();
return (
<>
<Header addScriptedFieldUrl={helpers.getRouteHref(indexPattern, 'addField')} />
<Header
addScriptedFieldUrl={`${window.location.origin +
window.location.pathname}#/management/kibana/index_patterns/${
indexPattern.id
}/create-field/`}
/>
<CallOuts
deprecatedLangsInUse={deprecatedLangsInUse}

View file

@ -0,0 +1,20 @@
/*
* 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.
*/
export { Tabs } from './tabs';

View file

@ -0,0 +1,267 @@
/*
* 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 React, { useState, useCallback, useEffect, Fragment, useMemo } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import {
EuiFlexGroup,
EuiFlexItem,
EuiTabbedContent,
EuiTabbedContentTab,
EuiSpacer,
EuiFieldSearch,
EuiSelect,
EuiSelectOption,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { fieldWildcardMatcher } from '../../../../../../../../../plugins/kibana_utils/public';
import { IndexPatternManagementStart } from '../../../../../../../../../plugins/index_pattern_management/public';
import { IndexPattern, IndexPatternField } from '../../../../../../../../../plugins/data/public';
import { createEditIndexPatternPageStateContainer } from '../edit_index_pattern_state_container';
import { TAB_INDEXED_FIELDS, TAB_SCRIPTED_FIELDS, TAB_SOURCE_FILTERS } from '../constants';
import { SourceFiltersTable } from '../source_filters_table';
import { IndexedFieldsTable } from '../indexed_fields_table';
import { ScriptedFieldsTable } from '../scripted_fields_table';
import { getTabs, getPath, convertToEuiSelectOption } from './utils';
interface TabsProps extends Pick<RouteComponentProps, 'history' | 'location'> {
indexPattern: IndexPattern;
config: Record<string, any>;
fields: IndexPatternField[];
services: {
indexPatternManagement: IndexPatternManagementStart;
};
}
const filterAriaLabel = i18n.translate('kbn.management.editIndexPattern.fields.filterAria', {
defaultMessage: 'Filter',
});
const filterPlaceholder = i18n.translate(
'kbn.management.editIndexPattern.fields.filterPlaceholder',
{
defaultMessage: 'Filter',
}
);
export function Tabs({ config, indexPattern, fields, services, history, location }: TabsProps) {
const [fieldFilter, setFieldFilter] = useState<string>('');
const [indexedFieldTypeFilter, setIndexedFieldTypeFilter] = useState<string>('');
const [scriptedFieldLanguageFilter, setScriptedFieldLanguageFilter] = useState<string>('');
const [indexedFieldTypes, setIndexedFieldType] = useState<EuiSelectOption[]>([]);
const [scriptedFieldLanguages, setScriptedFieldLanguages] = useState<EuiSelectOption[]>([]);
const [syncingStateFunc, setSyncingStateFunc] = useState<any>({
getCurrentTab: () => TAB_INDEXED_FIELDS,
});
const refreshFilters = useCallback(() => {
const tempIndexedFieldTypes: string[] = [];
const tempScriptedFieldLanguages: string[] = [];
indexPattern.fields.forEach(field => {
if (field.scripted) {
if (field.lang) {
tempScriptedFieldLanguages.push(field.lang);
}
} else {
tempIndexedFieldTypes.push(field.type);
}
});
setIndexedFieldType(convertToEuiSelectOption(tempIndexedFieldTypes, 'indexedFiledTypes'));
setScriptedFieldLanguages(
convertToEuiSelectOption(tempScriptedFieldLanguages, 'scriptedFieldLanguages')
);
}, [indexPattern]);
useEffect(() => {
refreshFilters();
}, [indexPattern, indexPattern.fields, refreshFilters]);
const fieldWildcardMatcherDecorated = useCallback(
(filters: string[]) => fieldWildcardMatcher(filters, config.get('metaFields')),
[config]
);
const getFilterSection = useCallback(
(type: string) => {
return (
<EuiFlexGroup>
<EuiFlexItem grow={true}>
<EuiFieldSearch
placeholder={filterPlaceholder}
value={fieldFilter}
onChange={e => setFieldFilter(e.target.value)}
data-test-subj="indexPatternFieldFilter"
aria-label={filterAriaLabel}
/>
</EuiFlexItem>
{type === TAB_INDEXED_FIELDS && indexedFieldTypes.length > 0 && (
<EuiFlexItem grow={false}>
<EuiSelect
options={indexedFieldTypes}
value={indexedFieldTypeFilter}
onChange={e => setIndexedFieldTypeFilter(e.target.value)}
data-test-subj="indexedFieldTypeFilterDropdown"
/>
</EuiFlexItem>
)}
{type === TAB_SCRIPTED_FIELDS && scriptedFieldLanguages.length > 0 && (
<EuiFlexItem grow={false}>
<EuiSelect
options={scriptedFieldLanguages}
value={scriptedFieldLanguageFilter}
onChange={e => setScriptedFieldLanguageFilter(e.target.value)}
data-test-subj="scriptedFieldLanguageFilterDropdown"
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
},
[
fieldFilter,
indexedFieldTypeFilter,
indexedFieldTypes,
scriptedFieldLanguageFilter,
scriptedFieldLanguages,
]
);
const getContent = useCallback(
(type: string) => {
switch (type) {
case TAB_INDEXED_FIELDS:
return (
<Fragment>
<EuiSpacer size="m" />
{getFilterSection(type)}
<EuiSpacer size="m" />
<IndexedFieldsTable
fields={fields}
indexPattern={indexPattern}
fieldFilter={fieldFilter}
fieldWildcardMatcher={fieldWildcardMatcherDecorated}
indexedFieldTypeFilter={indexedFieldTypeFilter}
helpers={{
redirectToRoute: (field: IndexPatternField) => {
history.push(getPath(field));
},
getFieldInfo: services.indexPatternManagement.list.getFieldInfo,
}}
/>
</Fragment>
);
case TAB_SCRIPTED_FIELDS:
return (
<Fragment>
<EuiSpacer size="m" />
{getFilterSection(type)}
<EuiSpacer size="m" />
<ScriptedFieldsTable
indexPattern={indexPattern}
fieldFilter={fieldFilter}
scriptedFieldLanguageFilter={scriptedFieldLanguageFilter}
helpers={{
redirectToRoute: (field: IndexPatternField) => {
history.push(getPath(field));
},
}}
onRemoveField={refreshFilters}
/>
</Fragment>
);
case TAB_SOURCE_FILTERS:
return (
<Fragment>
<EuiSpacer size="m" />
{getFilterSection(type)}
<EuiSpacer size="m" />
<SourceFiltersTable
indexPattern={indexPattern}
filterFilter={fieldFilter}
fieldWildcardMatcher={fieldWildcardMatcherDecorated}
onAddOrRemoveFilter={refreshFilters}
/>
</Fragment>
);
}
},
[
fieldFilter,
fieldWildcardMatcherDecorated,
fields,
getFilterSection,
history,
indexPattern,
indexedFieldTypeFilter,
refreshFilters,
scriptedFieldLanguageFilter,
services.indexPatternManagement.list.getFieldInfo,
]
);
const euiTabs: EuiTabbedContentTab[] = useMemo(
() =>
getTabs(indexPattern, fieldFilter, services.indexPatternManagement.list).map(
(tab: Pick<EuiTabbedContentTab, 'name' | 'id'>) => {
return {
...tab,
content: getContent(tab.id),
};
}
),
[fieldFilter, getContent, indexPattern, services.indexPatternManagement.list]
);
const [selectedTabId, setSelectedTabId] = useState(euiTabs[0].id);
useEffect(() => {
const {
startSyncingState,
stopSyncingState,
setCurrentTab,
getCurrentTab,
} = createEditIndexPatternPageStateContainer({
useHashedUrl: config.get('state:storeInSessionStorage'),
defaultTab: TAB_INDEXED_FIELDS,
});
startSyncingState();
setSyncingStateFunc({
setCurrentTab,
getCurrentTab,
});
setSelectedTabId(getCurrentTab());
return () => {
stopSyncingState();
};
}, [config]);
return (
<EuiTabbedContent
tabs={euiTabs}
selectedTab={euiTabs.find(tab => tab.id === selectedTabId)}
onTabClick={tab => {
setSelectedTabId(tab.id);
syncingStateFunc.setCurrentTab(tab.id);
}}
/>
);
}

View file

@ -0,0 +1,146 @@
/*
* 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 { Dictionary, countBy, defaults, unique } from 'lodash';
import { i18n } from '@kbn/i18n';
import { IndexPattern, IndexPatternField } from '../../../../../../../../../plugins/data/public';
import { IndexPatternManagementStart } from '../../../../../../../../../plugins/index_pattern_management/public';
import { TAB_INDEXED_FIELDS, TAB_SCRIPTED_FIELDS, TAB_SOURCE_FILTERS } from '../constants';
function filterByName(items: IndexPatternField[], filter: string) {
const lowercaseFilter = (filter || '').toLowerCase();
return items.filter(item => item.name.toLowerCase().includes(lowercaseFilter));
}
function getCounts(
fields: IndexPatternField[],
sourceFilters: {
excludes: string[];
},
fieldFilter = ''
) {
const fieldCount = countBy(filterByName(fields, fieldFilter), function(field) {
return field.scripted ? 'scripted' : 'indexed';
});
defaults(fieldCount, {
indexed: 0,
scripted: 0,
sourceFilters: sourceFilters.excludes
? sourceFilters.excludes.filter(value =>
value.toLowerCase().includes(fieldFilter.toLowerCase())
).length
: 0,
});
return fieldCount;
}
function getTitle(type: string, filteredCount: Dictionary<number>, totalCount: Dictionary<number>) {
let title = '';
switch (type) {
case 'indexed':
title = i18n.translate('kbn.management.editIndexPattern.tabs.fieldsHeader', {
defaultMessage: 'Fields',
});
break;
case 'scripted':
title = i18n.translate('kbn.management.editIndexPattern.tabs.scriptedHeader', {
defaultMessage: 'Scripted fields',
});
break;
case 'sourceFilters':
title = i18n.translate('kbn.management.editIndexPattern.tabs.sourceHeader', {
defaultMessage: 'Source filters',
});
break;
}
const count = ` (${
filteredCount[type] === totalCount[type]
? filteredCount[type]
: filteredCount[type] + ' / ' + totalCount[type]
})`;
return title + count;
}
export function getTabs(
indexPattern: IndexPattern,
fieldFilter: string,
indexPatternListProvider: IndexPatternManagementStart['list']
) {
const totalCount = getCounts(indexPattern.fields, indexPattern.getSourceFiltering());
const filteredCount = getCounts(
indexPattern.fields,
indexPattern.getSourceFiltering(),
fieldFilter
);
const tabs = [];
tabs.push({
name: getTitle('indexed', filteredCount, totalCount),
id: TAB_INDEXED_FIELDS,
});
if (indexPatternListProvider.areScriptedFieldsEnabled(indexPattern)) {
tabs.push({
name: getTitle('scripted', filteredCount, totalCount),
id: TAB_SCRIPTED_FIELDS,
});
}
tabs.push({
name: getTitle('sourceFilters', filteredCount, totalCount),
id: TAB_SOURCE_FILTERS,
});
return tabs;
}
export function getPath(field: IndexPatternField) {
return `/management/kibana/index_patterns/${field.indexPattern?.id}/field/${field.name}`;
}
const allTypesDropDown = i18n.translate('kbn.management.editIndexPattern.fields.allTypesDropDown', {
defaultMessage: 'All field types',
});
const allLangsDropDown = i18n.translate('kbn.management.editIndexPattern.fields.allLangsDropDown', {
defaultMessage: 'All languages',
});
export function convertToEuiSelectOption(options: string[], type: string) {
const euiOptions =
options.length > 0
? [
{
value: '',
text: type === 'scriptedFieldLanguages' ? allLangsDropDown : allTypesDropDown,
},
]
: [];
return euiOptions.concat(
unique(options).map(option => {
return {
value: option,
text: option,
};
})
);
}

View file

@ -206,15 +206,17 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider
async getFieldsTabCount() {
return retry.try(async () => {
const text = await testSubjects.getVisibleText('tab-count-indexedFields');
return text.replace(/\((.*)\)/, '$1');
const indexedFieldsTab = await find.byCssSelector('#indexedFields .euiTab__content');
const text = await indexedFieldsTab.getVisibleText();
return text.split(/[()]/)[1];
});
}
async getScriptedFieldsTabCount() {
return await retry.try(async () => {
const theText = await testSubjects.getVisibleText('tab-count-scriptedFields');
return theText.replace(/\((.*)\)/, '$1');
const scriptedFieldsTab = await find.byCssSelector('#scriptedFields .euiTab__content');
const text = await scriptedFieldsTab.getVisibleText();
return text.split(/[()]/)[1];
});
}
@ -241,13 +243,13 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider
async setFieldTypeFilter(type: string) {
await find.clickByCssSelector(
'select[data-test-subj="indexedFieldTypeFilterDropdown"] > option[label="' + type + '"]'
'select[data-test-subj="indexedFieldTypeFilterDropdown"] > option[value="' + type + '"]'
);
}
async setScriptedFieldLanguageFilter(language: string) {
await find.clickByCssSelector(
'select[data-test-subj="scriptedFieldLanguageFilterDropdown"] > option[label="' +
'select[data-test-subj="scriptedFieldLanguageFilterDropdown"] > option[value="' +
language +
'"]'
);
@ -412,17 +414,17 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider
async clickFieldsTab() {
log.debug('click Fields tab');
await testSubjects.click('tab-indexFields');
await find.clickByCssSelector('#indexedFields');
}
async clickScriptedFieldsTab() {
log.debug('click Scripted Fields tab');
await testSubjects.click('tab-scriptedFields');
await find.clickByCssSelector('#scriptedFields');
}
async clickSourceFiltersTab() {
log.debug('click Source Filters tab');
await testSubjects.click('tab-sourceFilters');
await find.clickByCssSelector('#sourceFilters');
}
async editScriptedField(name: string) {

View file

@ -2186,10 +2186,8 @@
"kbn.management.createIndexPatternHeader": "{indexPatternName} の作成",
"kbn.management.createIndexPatternLabel": "Kibana は、可視化などを目的に Elasticsearch インデックスからデータを取得するために、インデックスパターンを使用します。",
"kbn.management.editIndexPattern.deleteButton": "削除",
"kbn.management.editIndexPattern.deleteFieldButton": "削除",
"kbn.management.editIndexPattern.deleteHeader": "インデックスパターンを削除しますか?",
"kbn.management.editIndexPattern.detailsAria": "インデックスパターンの詳細",
"kbn.management.editIndexPattern.editFieldButton": "編集",
"kbn.management.editIndexPattern.fields.allLangsDropDown": "すべての言語",
"kbn.management.editIndexPattern.fields.allTypesDropDown": "すべてのフィールドタイプ",
"kbn.management.editIndexPattern.fields.filterAria": "フィルター",
@ -2215,7 +2213,6 @@
"kbn.management.editIndexPattern.fields.table.typeHeader": "タイプ",
"kbn.management.editIndexPattern.mappingConflictHeader": "マッピングの矛盾",
"kbn.management.editIndexPattern.mappingConflictLabel": "{conflictFieldsLength, plural, one {フィールドが} other {# フィールドが}}このパターンと一致するインデックスの間で異なるタイプ (文字列、整数など) に定義されています。これらの矛盾したフィールドは Kibana の一部で使用できますが、Kibana がタイプを把握しなければならない機能には使用できません。この問題を修正するにはデータのレンダリングが必要です。",
"kbn.management.editIndexPattern.notDateErrorMessage": "このフィールドは日付ではなく {fieldType} です。",
"kbn.management.editIndexPattern.refreshAria": "フィールドリストを再度読み込みます",
"kbn.management.editIndexPattern.refreshButton": "更新",
"kbn.management.editIndexPattern.refreshHeader": "フィールドリストを更新しますか?",

View file

@ -2187,10 +2187,8 @@
"kbn.management.createIndexPatternHeader": "创建 {indexPatternName}",
"kbn.management.createIndexPatternLabel": "Kibana 使用索引模式从 Elasticsearch 索引中检索数据,以实现诸如可视化等功能。",
"kbn.management.editIndexPattern.deleteButton": "删除",
"kbn.management.editIndexPattern.deleteFieldButton": "删除",
"kbn.management.editIndexPattern.deleteHeader": "删除索引模式?",
"kbn.management.editIndexPattern.detailsAria": "索引模式详细信息",
"kbn.management.editIndexPattern.editFieldButton": "编辑",
"kbn.management.editIndexPattern.fields.allLangsDropDown": "所有语言",
"kbn.management.editIndexPattern.fields.allTypesDropDown": "所有字段类型",
"kbn.management.editIndexPattern.fields.filterAria": "筛选",
@ -2216,7 +2214,6 @@
"kbn.management.editIndexPattern.fields.table.typeHeader": "类型",
"kbn.management.editIndexPattern.mappingConflictHeader": "映射冲突",
"kbn.management.editIndexPattern.mappingConflictLabel": "匹配此模式的各个索引中{conflictFieldsLength, plural, one {一个字段已} other {# 个字段已}}定义为若干类型(字符串、整数等)。您仍能够在 Kibana 的各个部分中使用这些冲突类型,但它们将无法用于需要 Kibana 知道其类型的函数。要解决此问题,需要重新索引您的数据。",
"kbn.management.editIndexPattern.notDateErrorMessage": "该字段是{fieldType},不是日期。",
"kbn.management.editIndexPattern.refreshAria": "重新加载字段列表",
"kbn.management.editIndexPattern.refreshButton": "刷新",
"kbn.management.editIndexPattern.refreshHeader": "刷新字段列表?",