migrate saved objects management edition view to react/typescript/eui (#59490)

* migrate so management edition view to react

* fix bundle name + add forgotten data-test-subj

* add FTR tests for edition page

* EUIfy react components

* wrap form with EuiPanel + caps btns labels

* Wrapping whole view in page content panel and removing legacy classes

* improve delete confirmation modal

* update translations

* improve delete popin

* add unit test on view components

* remove kui classes & address comments

* extract createFieldList and add tests

* disable form submit during submition

Co-authored-by: cchaos <caroline.horn@elastic.co>
This commit is contained in:
Pierre Gayvallet 2020-03-19 14:09:44 +01:00 committed by GitHub
parent 6ed2918b6c
commit 395d621249
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 2675 additions and 475 deletions

View file

@ -69,6 +69,7 @@ export interface OverlayModalConfirmOptions {
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
defaultFocusedButton?: EuiConfirmModalProps['defaultFocusedButton'];
buttonColor?: EuiConfirmModalProps['buttonColor'];
}
/**

View file

@ -35,9 +35,15 @@ interface SavedObjectRegistryEntry {
title: string;
}
export interface ISavedObjectsManagementRegistry {
register(service: SavedObjectRegistryEntry): void;
all(): SavedObjectRegistryEntry[];
get(id: string): SavedObjectRegistryEntry | undefined;
}
const registry: SavedObjectRegistryEntry[] = [];
export const savedObjectManagementRegistry = {
export const savedObjectManagementRegistry: ISavedObjectsManagementRegistry = {
register: (service: SavedObjectRegistryEntry) => {
registry.push(service);
},

View file

@ -28,7 +28,6 @@ import { ObjectsTable } from './components/objects_table';
import { I18nContext } from 'ui/i18n';
import { get } from 'lodash';
import { npStart } from 'ui/new_platform';
import { getIndexBreadcrumbs } from './breadcrumbs';
const REACT_OBJECTS_TABLE_DOM_ELEMENT_ID = 'reactSavedObjectsTable';

View file

@ -1,203 +1,5 @@
<kbn-management-app section="kibana/objects" class="kuiView" data-test-subj="savedObjectsEdit">
<kbn-management-objects-view class="kuiViewContent kuiViewContent--constrainedWidth">
<!-- Header -->
<div class="kuiViewContentItem kuiBar kuiVerticalRhythm">
<div class="kuiBarSection">
<h1
class="kuiTitle"
i18n-id="kbn.management.objects.view.editItemTitle"
i18n-default-message="Edit {title}"
i18n-values="{ title }"
ng-if="canEdit"
></h1>
<h1
class="kuiTitle"
i18n-id="kbn.management.objects.view.viewItemTitle"
i18n-default-message="View {title}"
i18n-values="{ title }"
ng-if="!canEdit"
></h1>
</div>
<div class="kuiBarSection">
<a
class="kuiButton kuiButton--basic kuiButton--iconText"
href="{{ link }}"
ng-if="canViewInApp"
>
<span class="kuiButton__inner">
<span class="kuiButton__icon kuiIcon fa-eye"></span>
<span
i18n-id="kbn.management.objects.view.viewItemButtonLabel"
i18n-default-message="View {title}"
i18n-values="{ title }"
></span>
</span>
</a>
<button
class="kuiButton kuiButton--danger kuiButton--iconText"
ng-click="delete()"
ng-if="canDelete"
data-test-subj="savedObjectEditDelete"
>
<span class="kuiButton__inner">
<span class="kuiButton__icon kuiIcon fa-trash-o"></span>
<span
i18n-id="kbn.management.objects.view.deleteItemButtonLabel"
i18n-default-message="Delete {title}"
i18n-values="{ title }"
></span>
</span>
</button>
</div>
</div>
<!-- Errors -->
<div
class="kuiViewContentItem kuiVerticalRhythm"
ng-if="notFound"
>
<div class="kuiInfoPanel kuiInfoPanel--error">
<div class="kuiInfoPanelHeader">
<span class="kuiInfoPanelHeader__icon kuiIcon kuiIcon--error fa-warning"></span>
<span
class="kuiInfoPanelHeader__title"
i18n-id="kbn.management.objects.view.savedObjectProblemErrorMessage"
i18n-default-message="There is a problem with this saved object"
></span>
</div>
<div class="kuiInfoPanelBody">
<div
class="kuiInfoPanelBody__message"
ng-if="notFound === 'search'"
i18n-id="kbn.management.objects.view.savedSearchDoesNotExistErrorMessage"
i18n-default-message="The saved search associated with this object no longer exists."
></div>
<div
class="kuiInfoPanelBody__message"
ng-if="notFound === 'index-pattern'"
i18n-id="kbn.management.objects.view.indexPatternDoesNotExistErrorMessage"
i18n-default-message="The index pattern associated with this object no longer exists."
></div>
<div
class="kuiInfoPanelBody__message"
ng-if="notFound === 'index-pattern-field'"
i18n-id="kbn.management.objects.view.fieldDoesNotExistErrorMessage"
i18n-default-message="A field associated with this object no longer exists in the index pattern."
></div>
<div
class="kuiInfoPanelBody__message"
i18n-id="kbn.management.objects.view.howToFixErrorDescription"
i18n-default-message="If you know what this error means, go ahead and fix it &mdash; otherwise click the delete button above."
></div>
</div>
</div>
</div>
<!-- Intro -->
<div class="kuiViewContentItem kuiVerticalRhythm">
<div class="kuiInfoPanel kuiInfoPanel--warning" ng-if="canEdit">
<div class="kuiInfoPanelHeader">
<span class="kuiInfoPanelHeader__icon kuiIcon kuiIcon--warning fa-bolt"></span>
<span
class="kuiInfoPanelHeader__title"
i18n-id="kbn.management.objects.view.howToModifyObjectTitle"
i18n-default-message="Proceed with caution!"
></span>
</div>
<div class="kuiInfoPanelBody">
<div
class="kuiInfoPanelBody__message"
i18n-id="kbn.management.objects.view.howToModifyObjectDescription"
i18n-default-message=" Modifying objects is for advanced users only. Object properties are not validated and invalid objects could cause errors, data loss, or worse. Unless someone with intimate knowledge of the code told you to be in here, you probably shouldn&rsquo;t be."
>
</div>
</div>
</div>
</div>
<div class="kuiViewContentItem kuiVerticalRhythm">
<!-- Form -->
<form
role="form"
name="objectForm"
ng-submit="submit()"
data-test-subj="savedObjectEditForm"
>
<div class="kuiFormSection" ng-repeat="field in fields">
<label for="{{ field.name }}" class="kuiFormLabel">
{{ field.name }}
</label>
<input
id="{{ field.name }}"
ng-if="field.type === 'number'"
class="kuiTextInput"
type="number"
ng-model="field.value"
ng-disabled="{{ !canEdit }}"
>
<textarea
id="{{ field.name }}"
ng-if="field.type === 'text'"
class="kuiTextArea"
rows="1"
msd-elastic=" "
ng-model="field.value"
ng-disabled="{{ !canEdit }}"
></textarea>
<input
ng-if="field.type === 'boolean'"
class="kuiCheckBox"
type="checkbox"
ng-model="field.value"
ng-checked="field.value"
ng-disabled="{{ !canEdit }}"
>
<div
ng-if="field.type === 'json' || field.type === 'array'"
kbn-ui-ace-keyboard-mod
ng-attr-readonly="{{ canEdit ? undefined : true }}"
ui-ace="{ onLoad: aceLoaded, mode: 'json' }"
id="{{field.name}}"
ng-model="field.value"
class="form-control"
></div>
</div>
</form>
<!-- Actions -->
<div class="kuiButtonGroup">
<button
class="kuiButton kuiButton--primary"
aria-label="{{ 'kbn.management.objects.view.saveButtonAriaLabel' | i18n: { defaultMessage: 'Save { title } Object', values: { title } } }}"
ng-click="submit()"
ng-disabled="objectForm.$invalid || aceInvalidEditors.length !==0"
i18n-id="kbn.management.objects.view.saveButtonLabel"
i18n-default-message="Save { title } Object"
i18n-values="{ title }"
ng-if="canEdit"
data-test-subj="savedObjectEditSave"
></button>
<button
class="kuiButton kuiButton--basic"
aria-label="{{ 'kbn.management.objects.view.cancelButtonAriaLabel' | i18n: { defaultMessage: 'Cancel'} }}"
ng-click="cancel()"
i18n-id="kbn.management.objects.view.cancelButtonLabel"
i18n-default-message="Cancel"
></button>
</div>
</div>
<kbn-management-app section="kibana/objects" data-test-subj="savedObjectsEdit">
<kbn-management-objects-view>
<div id="reactSavedObjectsView"></div>
</kbn-management-objects-view>
</kbn-management-app>

View file

@ -17,26 +17,20 @@
* under the License.
*/
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import angular from 'angular';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import 'angular';
import 'angular-elastic/elastic';
import rison from 'rison-node';
import { savedObjectManagementRegistry } from '../../saved_object_registry';
import objectViewHTML from './_view.html';
import uiRoutes from 'ui/routes';
import { uiModules } from 'ui/modules';
import { fatalError, toastNotifications } from 'ui/notify';
import 'ui/accessibility/kbn_ui_ace_keyboard_mode';
import { isNumeric } from './lib/numeric';
import { canViewInApp } from './lib/in_app_url';
import { I18nContext } from 'ui/i18n';
import { npStart } from 'ui/new_platform';
import { castEsToKbnFieldTypeName } from '../../../../../../../plugins/data/public';
import objectViewHTML from './_view.html';
import { getViewBreadcrumbs } from './breadcrumbs';
import { savedObjectManagementRegistry } from '../../saved_object_registry';
import { SavedObjectEdition } from './saved_object_view';
const location = 'SavedObject view';
const REACT_OBJECTS_VIEW_DOM_ELEMENT_ID = 'reactSavedObjectsView';
uiRoutes.when('/management/kibana/objects/:service/:id', {
template: objectViewHTML,
@ -44,261 +38,48 @@ uiRoutes.when('/management/kibana/objects/:service/:id', {
requireUICapability: 'management.kibana.objects',
});
function createReactView($scope, $routeParams) {
const { service: serviceName, id: objectId, notFound } = $routeParams;
const { savedObjects, overlays, notifications, application } = npStart.core;
$scope.$$postDigest(() => {
const node = document.getElementById(REACT_OBJECTS_VIEW_DOM_ELEMENT_ID);
if (!node) {
return;
}
render(
<I18nContext>
<SavedObjectEdition
id={objectId}
serviceName={serviceName}
serviceRegistry={savedObjectManagementRegistry}
savedObjectsClient={savedObjects.client}
overlays={overlays}
notifications={notifications}
capabilities={application.capabilities}
notFoundType={notFound}
/>
</I18nContext>,
node
);
});
}
function destroyReactView() {
const node = document.getElementById(REACT_OBJECTS_VIEW_DOM_ELEMENT_ID);
node && unmountComponentAtNode(node);
}
uiModules
.get('apps/management', ['monospaced.elastic'])
.directive('kbnManagementObjectsView', function() {
return {
restrict: 'E',
controller: function($scope, $routeParams, $location, $window, $rootScope, uiCapabilities) {
const serviceObj = savedObjectManagementRegistry.get($routeParams.service);
const service = serviceObj.service;
const savedObjectsClient = npStart.core.savedObjects.client;
const { overlays } = npStart.core;
/**
* Creates a field definition and pushes it to the memo stack. This function
* is designed to be used in conjunction with _.reduce(). If the
* values is plain object it will recurse through all the keys till it hits
* a string, number or an array.
*
* @param {array} memo The stack of fields
* @param {mixed} value The value of the field
* @param {string} key The key of the field
* @param {object} collection This is a reference the collection being reduced
* @param {array} parents The parent keys to the field
* @returns {array}
*/
const createField = function(memo, val, key, collection, parents) {
if (Array.isArray(parents)) {
parents.push(key);
} else {
parents = [key];
}
const field = { type: 'text', name: parents.join('.'), value: val };
if (_.isString(field.value)) {
try {
field.value = angular.toJson(JSON.parse(field.value), true);
field.type = 'json';
} catch (err) {
field.value = field.value;
}
} else if (isNumeric(field.value)) {
field.type = 'number';
} else if (Array.isArray(field.value)) {
field.type = 'array';
field.value = angular.toJson(field.value, true);
} else if (_.isBoolean(field.value)) {
field.type = 'boolean';
field.value = field.value;
} else if (_.isPlainObject(field.value)) {
// do something recursive
return _.reduce(field.value, _.partialRight(createField, parents), memo);
}
memo.push(field);
// once the field is added to the object you need to pop the parents
// to remove it since we've hit the end of the branch.
parents.pop();
return memo;
};
const readObjectClass = function(fields, Class) {
const fieldMap = _.indexBy(fields, 'name');
_.forOwn(Class.mapping, function(esType, name) {
if (fieldMap[name]) return;
fields.push({
name: name,
type: (function() {
switch (castEsToKbnFieldTypeName(esType)) {
case 'string':
return 'text';
case 'number':
return 'number';
case 'boolean':
return 'boolean';
default:
return 'json';
}
})(),
});
});
if (Class.searchSource && !fieldMap['kibanaSavedObjectMeta.searchSourceJSON']) {
fields.push({
name: 'kibanaSavedObjectMeta.searchSourceJSON',
type: 'json',
value: '{}',
});
}
if (!fieldMap.references) {
fields.push({
name: 'references',
type: 'array',
value: '[]',
});
}
};
const { edit: canEdit, delete: canDelete } = uiCapabilities.savedObjectsManagement;
$scope.canEdit = canEdit;
$scope.canDelete = canDelete;
$scope.canViewInApp = canViewInApp(uiCapabilities, service.type);
$scope.notFound = $routeParams.notFound;
$scope.title = service.type;
savedObjectsClient
.get(service.type, $routeParams.id)
.then(function(obj) {
$scope.obj = obj;
$scope.link = service.urlFor(obj.id);
const fields = _.reduce(obj.attributes, createField, []);
// Special handling for references which isn't within "attributes"
createField(fields, obj.references, 'references');
if (service.Class) readObjectClass(fields, service.Class);
// sorts twice since we want numerical sort to prioritize over name,
// and sortBy will do string comparison if trying to match against strings
const nameSortedFields = _.sortBy(fields, 'name');
$scope.$evalAsync(() => {
$scope.fields = _.sortBy(nameSortedFields, field => {
const orderIndex = service.Class.fieldOrder
? service.Class.fieldOrder.indexOf(field.name)
: -1;
return orderIndex > -1 ? orderIndex : Infinity;
});
});
$scope.$digest();
})
.catch(error => fatalError(error, location));
// This handles the validation of the Ace Editor. Since we don't have any
// other hooks into the editors to tell us if the content is valid or not
// we need to use the annotations to see if they have any errors. If they
// do then we push the field.name to aceInvalidEditor variable.
// Otherwise we remove it.
const loadedEditors = [];
$scope.aceInvalidEditors = [];
$scope.aceLoaded = function(editor) {
if (_.contains(loadedEditors, editor)) return;
loadedEditors.push(editor);
editor.$blockScrolling = Infinity;
const session = editor.getSession();
const fieldName = editor.container.id;
session.setTabSize(2);
session.setUseSoftTabs(true);
session.on('changeAnnotation', function() {
const annotations = session.getAnnotations();
if (_.some(annotations, { type: 'error' })) {
if (!_.contains($scope.aceInvalidEditors, fieldName)) {
$scope.aceInvalidEditors.push(fieldName);
}
} else {
$scope.aceInvalidEditors = _.without($scope.aceInvalidEditors, fieldName);
}
if (!$rootScope.$$phase) $scope.$apply();
});
};
$scope.cancel = function() {
$window.history.back();
return false;
};
/**
* Deletes an object and sets the notification
* @param {type} name description
* @returns {type} description
*/
$scope.delete = function() {
function doDelete() {
savedObjectsClient
.delete(service.type, $routeParams.id)
.then(function() {
return redirectHandler('deleted');
})
.catch(error => fatalError(error, location));
}
const confirmModalOptions = {
confirmButtonText: i18n.translate(
'kbn.management.objects.confirmModalOptions.deleteButtonLabel',
{
defaultMessage: 'Delete',
}
),
title: i18n.translate('kbn.management.objects.confirmModalOptions.modalTitle', {
defaultMessage: 'Delete saved Kibana object?',
}),
};
overlays
.openConfirm(
i18n.translate('kbn.management.objects.confirmModalOptions.modalDescription', {
defaultMessage: "You can't recover deleted objects",
}),
confirmModalOptions
)
.then(isConfirmed => {
if (isConfirmed) {
doDelete();
}
});
};
$scope.submit = function() {
const source = _.cloneDeep($scope.obj.attributes);
_.each($scope.fields, function(field) {
let value = field.value;
if (field.type === 'number') {
value = Number(field.value);
}
if (field.type === 'array') {
value = JSON.parse(field.value);
}
_.set(source, field.name, value);
});
const { references, ...attributes } = source;
savedObjectsClient
.update(service.type, $routeParams.id, attributes, { references })
.then(function() {
return redirectHandler('updated');
})
.catch(error => fatalError(error, location));
};
function redirectHandler(action) {
$location.path('/management/kibana/objects').search({
_a: rison.encode({
tab: serviceObj.title,
}),
});
toastNotifications.addSuccess(
`${_.capitalize(action)} '${
$scope.obj.attributes.title
}' ${$scope.title.toLowerCase()} object`
);
}
controller: function($scope, $routeParams) {
createReactView($scope, $routeParams);
$scope.$on('$destroy', destroyReactView);
},
};
});

View file

@ -0,0 +1,165 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Intro component renders correctly 1`] = `
<Header
canDelete={true}
canEdit={true}
canViewInApp={true}
onDeleteClick={[Function]}
type="search"
viewUrl="/some-url"
>
<EuiPageContentHeader>
<div
className="euiPageContentHeader euiPageContentHeader--responsive"
>
<EuiPageContentHeaderSection>
<div
className="euiPageContentHeaderSection"
>
<EuiTitle>
<h1
className="euiTitle euiTitle--medium"
>
<FormattedMessage
defaultMessage="Edit {title}"
id="kbn.management.objects.view.editItemTitle"
values={
Object {
"title": "search",
}
}
>
Edit search
</FormattedMessage>
</h1>
</EuiTitle>
</div>
</EuiPageContentHeaderSection>
<EuiPageContentHeaderSection>
<div
className="euiPageContentHeaderSection"
>
<EuiFlexGroup
responsive={false}
>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow"
>
<EuiFlexItem
grow={false}
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<EuiButton
data-test-subj="savedObjectEditViewInApp"
href="/some-url"
iconType="eye"
size="s"
>
<a
className="euiButton euiButton--primary euiButton--small"
data-test-subj="savedObjectEditViewInApp"
href="/some-url"
rel="noreferrer"
>
<span
className="euiButton__content"
>
<EuiIcon
aria-hidden="true"
className="euiButton__icon"
size="m"
type="eye"
>
<div
aria-hidden="true"
className="euiButton__icon"
data-euiicon-type="eye"
size="m"
/>
</EuiIcon>
<span
className="euiButton__text"
>
<FormattedMessage
defaultMessage="View {title}"
id="kbn.management.objects.view.viewItemButtonLabel"
values={
Object {
"title": "search",
}
}
>
View search
</FormattedMessage>
</span>
</span>
</a>
</EuiButton>
</div>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<EuiButton
color="danger"
data-test-subj="savedObjectEditDelete"
iconType="trash"
onClick={[Function]}
size="s"
>
<button
className="euiButton euiButton--danger euiButton--small"
data-test-subj="savedObjectEditDelete"
onClick={[Function]}
type="button"
>
<span
className="euiButton__content"
>
<EuiIcon
aria-hidden="true"
className="euiButton__icon"
size="m"
type="trash"
>
<div
aria-hidden="true"
className="euiButton__icon"
data-euiicon-type="trash"
size="m"
/>
</EuiIcon>
<span
className="euiButton__text"
>
<FormattedMessage
defaultMessage="Delete {title}"
id="kbn.management.objects.view.deleteItemButtonLabel"
values={
Object {
"title": "search",
}
}
>
Delete search
</FormattedMessage>
</span>
</span>
</button>
</EuiButton>
</div>
</EuiFlexItem>
</div>
</EuiFlexGroup>
</div>
</EuiPageContentHeaderSection>
</div>
</EuiPageContentHeader>
</Header>
`;

View file

@ -0,0 +1,67 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Intro component renders correctly 1`] = `
<Intro>
<EuiCallOut
color="warning"
iconType="alert"
title={
<FormattedMessage
defaultMessage="Proceed with caution!"
id="kbn.management.objects.view.howToModifyObjectTitle"
values={Object {}}
/>
}
>
<div
className="euiCallOut euiCallOut--warning"
>
<div
className="euiCallOutHeader"
>
<EuiIcon
aria-hidden="true"
className="euiCallOutHeader__icon"
size="m"
type="alert"
>
<div
aria-hidden="true"
className="euiCallOutHeader__icon"
data-euiicon-type="alert"
size="m"
/>
</EuiIcon>
<span
className="euiCallOutHeader__title"
>
<FormattedMessage
defaultMessage="Proceed with caution!"
id="kbn.management.objects.view.howToModifyObjectTitle"
values={Object {}}
>
Proceed with caution!
</FormattedMessage>
</span>
</div>
<EuiText
size="s"
>
<div
className="euiText euiText--small"
>
<div>
<FormattedMessage
defaultMessage="Modifying objects is for advanced users only. Object properties are not validated and invalid objects could cause errors, data loss, or worse. Unless someone with intimate knowledge of the code told you to be in here, you probably shouldnt be."
id="kbn.management.objects.view.howToModifyObjectDescription"
values={Object {}}
>
Modifying objects is for advanced users only. Object properties are not validated and invalid objects could cause errors, data loss, or worse. Unless someone with intimate knowledge of the code told you to be in here, you probably shouldnt be.
</FormattedMessage>
</div>
</div>
</EuiText>
</div>
</EuiCallOut>
</Intro>
`;

View file

@ -0,0 +1,301 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NotFoundErrors component renders correctly for index-pattern type 1`] = `
<NotFoundErrors
type="index-pattern"
>
<EuiCallOut
color="danger"
iconType="alert"
title={
<FormattedMessage
defaultMessage="There is a problem with this saved object"
id="kbn.management.objects.view.savedObjectProblemErrorMessage"
values={Object {}}
/>
}
>
<div
className="euiCallOut euiCallOut--danger"
>
<div
className="euiCallOutHeader"
>
<EuiIcon
aria-hidden="true"
className="euiCallOutHeader__icon"
size="m"
type="alert"
>
<div
aria-hidden="true"
className="euiCallOutHeader__icon"
data-euiicon-type="alert"
size="m"
/>
</EuiIcon>
<span
className="euiCallOutHeader__title"
>
<FormattedMessage
defaultMessage="There is a problem with this saved object"
id="kbn.management.objects.view.savedObjectProblemErrorMessage"
values={Object {}}
>
There is a problem with this saved object
</FormattedMessage>
</span>
</div>
<EuiText
size="s"
>
<div
className="euiText euiText--small"
>
<div>
<FormattedMessage
defaultMessage="The index pattern associated with this object no longer exists."
id="kbn.management.objects.view.indexPatternDoesNotExistErrorMessage"
values={Object {}}
>
The index pattern associated with this object no longer exists.
</FormattedMessage>
</div>
<div>
<FormattedMessage
defaultMessage="If you know what this error means, go ahead and fix it — otherwise click the delete button above."
id="kbn.management.objects.view.howToFixErrorDescription"
values={Object {}}
>
If you know what this error means, go ahead and fix it — otherwise click the delete button above.
</FormattedMessage>
</div>
</div>
</EuiText>
</div>
</EuiCallOut>
</NotFoundErrors>
`;
exports[`NotFoundErrors component renders correctly for index-pattern-field type 1`] = `
<NotFoundErrors
type="index-pattern-field"
>
<EuiCallOut
color="danger"
iconType="alert"
title={
<FormattedMessage
defaultMessage="There is a problem with this saved object"
id="kbn.management.objects.view.savedObjectProblemErrorMessage"
values={Object {}}
/>
}
>
<div
className="euiCallOut euiCallOut--danger"
>
<div
className="euiCallOutHeader"
>
<EuiIcon
aria-hidden="true"
className="euiCallOutHeader__icon"
size="m"
type="alert"
>
<div
aria-hidden="true"
className="euiCallOutHeader__icon"
data-euiicon-type="alert"
size="m"
/>
</EuiIcon>
<span
className="euiCallOutHeader__title"
>
<FormattedMessage
defaultMessage="There is a problem with this saved object"
id="kbn.management.objects.view.savedObjectProblemErrorMessage"
values={Object {}}
>
There is a problem with this saved object
</FormattedMessage>
</span>
</div>
<EuiText
size="s"
>
<div
className="euiText euiText--small"
>
<div>
<FormattedMessage
defaultMessage="A field associated with this object no longer exists in the index pattern."
id="kbn.management.objects.view.fieldDoesNotExistErrorMessage"
values={Object {}}
>
A field associated with this object no longer exists in the index pattern.
</FormattedMessage>
</div>
<div>
<FormattedMessage
defaultMessage="If you know what this error means, go ahead and fix it — otherwise click the delete button above."
id="kbn.management.objects.view.howToFixErrorDescription"
values={Object {}}
>
If you know what this error means, go ahead and fix it — otherwise click the delete button above.
</FormattedMessage>
</div>
</div>
</EuiText>
</div>
</EuiCallOut>
</NotFoundErrors>
`;
exports[`NotFoundErrors component renders correctly for search type 1`] = `
<NotFoundErrors
type="search"
>
<EuiCallOut
color="danger"
iconType="alert"
title={
<FormattedMessage
defaultMessage="There is a problem with this saved object"
id="kbn.management.objects.view.savedObjectProblemErrorMessage"
values={Object {}}
/>
}
>
<div
className="euiCallOut euiCallOut--danger"
>
<div
className="euiCallOutHeader"
>
<EuiIcon
aria-hidden="true"
className="euiCallOutHeader__icon"
size="m"
type="alert"
>
<div
aria-hidden="true"
className="euiCallOutHeader__icon"
data-euiicon-type="alert"
size="m"
/>
</EuiIcon>
<span
className="euiCallOutHeader__title"
>
<FormattedMessage
defaultMessage="There is a problem with this saved object"
id="kbn.management.objects.view.savedObjectProblemErrorMessage"
values={Object {}}
>
There is a problem with this saved object
</FormattedMessage>
</span>
</div>
<EuiText
size="s"
>
<div
className="euiText euiText--small"
>
<div>
<FormattedMessage
defaultMessage="The saved search associated with this object no longer exists."
id="kbn.management.objects.view.savedSearchDoesNotExistErrorMessage"
values={Object {}}
>
The saved search associated with this object no longer exists.
</FormattedMessage>
</div>
<div>
<FormattedMessage
defaultMessage="If you know what this error means, go ahead and fix it — otherwise click the delete button above."
id="kbn.management.objects.view.howToFixErrorDescription"
values={Object {}}
>
If you know what this error means, go ahead and fix it — otherwise click the delete button above.
</FormattedMessage>
</div>
</div>
</EuiText>
</div>
</EuiCallOut>
</NotFoundErrors>
`;
exports[`NotFoundErrors component renders correctly for unknown type 1`] = `
<NotFoundErrors
type="unknown"
>
<EuiCallOut
color="danger"
iconType="alert"
title={
<FormattedMessage
defaultMessage="There is a problem with this saved object"
id="kbn.management.objects.view.savedObjectProblemErrorMessage"
values={Object {}}
/>
}
>
<div
className="euiCallOut euiCallOut--danger"
>
<div
className="euiCallOutHeader"
>
<EuiIcon
aria-hidden="true"
className="euiCallOutHeader__icon"
size="m"
type="alert"
>
<div
aria-hidden="true"
className="euiCallOutHeader__icon"
data-euiicon-type="alert"
size="m"
/>
</EuiIcon>
<span
className="euiCallOutHeader__title"
>
<FormattedMessage
defaultMessage="There is a problem with this saved object"
id="kbn.management.objects.view.savedObjectProblemErrorMessage"
values={Object {}}
>
There is a problem with this saved object
</FormattedMessage>
</span>
</div>
<EuiText
size="s"
>
<div
className="euiText euiText--small"
>
<div />
<div>
<FormattedMessage
defaultMessage="If you know what this error means, go ahead and fix it — otherwise click the delete button above."
id="kbn.management.objects.view.howToFixErrorDescription"
values={Object {}}
>
If you know what this error means, go ahead and fix it — otherwise click the delete button above.
</FormattedMessage>
</div>
</div>
</EuiText>
</div>
</EuiCallOut>
</NotFoundErrors>
`;

View file

@ -0,0 +1,95 @@
/*
* 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 from 'react';
import { mount } from 'enzyme';
import { I18nProvider } from '@kbn/i18n/react';
import { Field } from './field';
import { FieldState, FieldType } from '../../types';
describe('Field component', () => {
const mountField = (props: {
type: FieldType;
name: string;
value: any;
disabled: boolean;
state?: FieldState;
onChange: (name: string, state: FieldState) => void;
}) =>
mount(
<I18nProvider>
<Field {...props} />
</I18nProvider>
).find('Field');
const defaultProps = {
type: 'text' as FieldType,
name: 'field',
value: '',
disabled: false,
state: undefined,
onChange: (name: string, state: FieldState) => undefined,
};
it('uses the field name as the label', () => {
let mounted = mountField({ ...defaultProps, name: 'some.name' });
expect(mounted.find('EuiFormLabel').text()).toMatchInlineSnapshot(`"some.name"`);
mounted = mountField({ ...defaultProps, name: 'someother.name' });
expect(mounted.find('EuiFormLabel').text()).toMatchInlineSnapshot(`"someother.name"`);
});
it('renders a EuiCodeEditor for json type', () => {
const mounted = mountField({ ...defaultProps, type: 'json' });
expect(mounted.exists('EuiCodeEditor')).toEqual(true);
});
it('renders a EuiCodeEditor for array type', () => {
const mounted = mountField({ ...defaultProps, type: 'array' });
expect(mounted.exists('EuiCodeEditor')).toEqual(true);
});
it('renders a EuiSwitch for boolean type', () => {
const mounted = mountField({ ...defaultProps, type: 'boolean' });
expect(mounted.exists('EuiSwitch')).toEqual(true);
});
it('display correct label for boolean type depending on value', () => {
let mounted = mountField({ ...defaultProps, type: 'boolean', value: true });
expect(mounted.find('EuiSwitch').text()).toMatchInlineSnapshot(`"On"`);
mounted = mountField({ ...defaultProps, type: 'boolean', value: false });
expect(mounted.find('EuiSwitch').text()).toMatchInlineSnapshot(`"Off"`);
});
it('renders a EuiFieldNumber for number type', () => {
const mounted = mountField({ ...defaultProps, type: 'number' });
expect(mounted.exists('EuiFieldNumber')).toEqual(true);
});
it('renders a EuiFieldText for text type', () => {
const mounted = mountField({ ...defaultProps, type: 'text' });
expect(mounted.exists('EuiFieldText')).toEqual(true);
});
it('renders a EuiFieldText as fallback', () => {
const mounted = mountField({ ...defaultProps, type: 'unknown-type' as any });
expect(mounted.exists('EuiFieldText')).toEqual(true);
});
});

View file

@ -0,0 +1,162 @@
/*
* 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, { PureComponent } from 'react';
import {
EuiFieldNumber,
EuiFieldText,
EuiFormRow,
EuiSwitch,
// @ts-ignore
EuiCodeEditor,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { FieldState, FieldType } from '../../types';
interface FieldProps {
type: FieldType;
name: string;
value: any;
disabled: boolean;
state?: FieldState;
onChange: (name: string, state: FieldState) => void;
}
export class Field extends PureComponent<FieldProps> {
render() {
const { name } = this.props;
return (
<EuiFormRow fullWidth={true} label={name}>
{this.renderField()}
</EuiFormRow>
);
}
onCodeEditorChange(targetValue: any) {
const { name, onChange } = this.props;
let invalid = false;
try {
JSON.parse(targetValue);
} catch (e) {
invalid = true;
}
onChange(name, {
value: targetValue,
invalid,
});
}
onFieldChange(targetValue: any) {
const { name, type, onChange } = this.props;
let newParsedValue = targetValue;
let invalid = false;
if (type === 'number') {
try {
newParsedValue = Number(newParsedValue);
} catch (e) {
invalid = true;
}
}
onChange(name, {
value: newParsedValue,
invalid,
});
}
renderField() {
const { type, name, state, disabled } = this.props;
const currentValue = state?.value ?? this.props.value;
switch (type) {
case 'number':
return (
<EuiFieldNumber
name={name}
id={this.fieldId}
value={currentValue}
onChange={e => this.onFieldChange(e.target.value)}
disabled={disabled}
data-test-subj={`savedObjects-editField-${name}`}
/>
);
case 'boolean':
return (
<EuiSwitch
name={name}
id={this.fieldId}
label={
!!currentValue ? (
<FormattedMessage id="kbn.management.objects.field.onLabel" defaultMessage="On" />
) : (
<FormattedMessage id="kbn.management.objects.field.offLabel" defaultMessage="Off" />
)
}
checked={!!currentValue}
onChange={e => this.onFieldChange(e.target.checked)}
disabled={disabled}
data-test-subj={`savedObjects-editField-${name}`}
/>
);
case 'json':
case 'array':
return (
<div data-test-subj={`savedObjects-editField-${name}`}>
<EuiCodeEditor
mode="json"
theme="textmate"
value={currentValue}
onChange={(value: any) => this.onCodeEditorChange(value)}
width="100%"
height="auto"
minLines={6}
maxLines={30}
isReadOnly={disabled}
setOptions={{
showLineNumbers: true,
tabSize: 2,
useSoftTabs: true,
}}
editorProps={{
$blockScrolling: Infinity,
}}
showGutter={true}
/>
</div>
);
default:
return (
<EuiFieldText
id={this.fieldId}
name={name}
value={currentValue}
onChange={e => this.onFieldChange(e.target.value)}
disabled={disabled}
data-test-subj={`savedObjects-editField-${name}`}
/>
);
}
}
private get fieldId() {
const { name } = this.props;
return `savedObjects-editField-${name}`;
}
}

View file

@ -0,0 +1,186 @@
/*
* 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, { Component } from 'react';
import {
EuiForm,
EuiFlexGroup,
EuiFlexItem,
EuiButton,
EuiButtonEmpty,
EuiSpacer,
} from '@elastic/eui';
import { cloneDeep, set } from 'lodash';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
SimpleSavedObject,
SavedObjectsClientContract,
} from '../../../../../../../../../core/public';
import { SavedObjectLoader } from '../../../../../../../../../plugins/saved_objects/public';
import { Field } from './field';
import { ObjectField, FieldState, SubmittedFormData } from '../../types';
import { createFieldList } from '../../lib/create_field_list';
interface FormProps {
object: SimpleSavedObject;
service: SavedObjectLoader;
savedObjectsClient: SavedObjectsClientContract;
editionEnabled: boolean;
onSave: (form: SubmittedFormData) => Promise<void>;
}
interface FormState {
fields: ObjectField[];
fieldStates: Record<string, FieldState>;
submitting: boolean;
}
export class Form extends Component<FormProps, FormState> {
constructor(props: FormProps) {
super(props);
this.state = {
fields: [],
fieldStates: {},
submitting: false,
};
}
componentDidMount() {
const { object, service } = this.props;
const fields = createFieldList(object, service);
this.setState({
fields,
});
}
render() {
const { editionEnabled, service } = this.props;
const { fields, fieldStates, submitting } = this.state;
const isValid = this.isFormValid();
return (
<EuiForm data-test-subj="savedObjectEditForm" role="form">
{fields.map(field => (
<Field
key={`${field.type}-${field.name}`}
type={field.type}
name={field.name}
value={field.value}
state={fieldStates[field.name]}
disabled={!editionEnabled}
onChange={this.handleFieldChange}
/>
))}
<EuiSpacer size={'l'} />
<EuiFlexGroup responsive={false} gutterSize={'m'}>
{editionEnabled && (
<EuiFlexItem grow={false}>
<EuiButton
fill={true}
aria-label={i18n.translate('kbn.management.objects.view.saveButtonAriaLabel', {
defaultMessage: 'Save { title } object',
values: {
title: service.type,
},
})}
onClick={this.onSubmit}
disabled={!isValid || submitting}
data-test-subj="savedObjectEditSave"
>
<FormattedMessage
id="kbn.management.objects.view.saveButtonLabel"
defaultMessage="Save { title } object"
values={{ title: service.type }}
/>
</EuiButton>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiButtonEmpty
aria-label={i18n.translate('kbn.management.objects.view.cancelButtonAriaLabel', {
defaultMessage: 'Cancel',
})}
onClick={this.onCancel}
data-test-subj="savedObjectEditCancel"
>
<FormattedMessage
id="kbn.management.objects.view.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiForm>
);
}
handleFieldChange = (name: string, newState: FieldState) => {
this.setState({
fieldStates: {
...this.state.fieldStates,
[name]: newState,
},
});
};
isFormValid() {
const { fieldStates } = this.state;
return !Object.values(fieldStates).some(state => state.invalid === true);
}
onCancel = () => {
window.history.back();
};
onSubmit = async () => {
const { object, onSave } = this.props;
const { fields, fieldStates } = this.state;
if (!this.isFormValid()) {
return;
}
this.setState({
submitting: true,
});
const source = cloneDeep(object.attributes as any);
fields.forEach(field => {
let value = fieldStates[field.name]?.value ?? field.value;
if (field.type === 'array' && typeof value === 'string') {
value = JSON.parse(value);
}
set(source, field.name, value);
});
const { references, ...attributes } = source;
await onSave({ attributes, references });
this.setState({
submitting: false,
});
};
}

View file

@ -0,0 +1,125 @@
/*
* 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 from 'react';
import { mount } from 'enzyme';
import { I18nProvider } from '@kbn/i18n/react';
import { Header } from './header';
describe('Intro component', () => {
const mountHeader = (props: {
canEdit: boolean;
canDelete: boolean;
canViewInApp: boolean;
type: string;
viewUrl: string;
onDeleteClick: () => void;
}) =>
mount(
<I18nProvider>
<Header {...props} />
</I18nProvider>
).find('Header');
const defaultProps = {
type: 'search',
canEdit: true,
canDelete: true,
canViewInApp: true,
viewUrl: '/some-url',
onDeleteClick: () => undefined,
};
it('renders correctly', () => {
const mounted = mountHeader({
...defaultProps,
});
expect(mounted).toMatchSnapshot();
});
it('displays correct title depending on canEdit', () => {
let mounted = mountHeader({
...defaultProps,
canEdit: true,
});
expect(mounted.find('h1').text()).toMatchInlineSnapshot(`"Edit search"`);
mounted = mountHeader({
...defaultProps,
canEdit: false,
});
expect(mounted.find('h1').text()).toMatchInlineSnapshot(`"View search"`);
});
it('displays correct title depending on type', () => {
let mounted = mountHeader({
...defaultProps,
type: 'some-type',
});
expect(mounted.find('h1').text()).toMatchInlineSnapshot(`"Edit some-type"`);
mounted = mountHeader({
...defaultProps,
type: 'another-type',
});
expect(mounted.find('h1').text()).toMatchInlineSnapshot(`"Edit another-type"`);
});
it('only displays delete button if canDelete is true', () => {
let mounted = mountHeader({
...defaultProps,
canDelete: true,
});
expect(mounted.exists(`button[data-test-subj='savedObjectEditDelete']`)).toBe(true);
mounted = mountHeader({
...defaultProps,
canDelete: false,
});
expect(mounted.exists(`button[data-test-subj='savedObjectEditDelete']`)).toBe(false);
});
it('calls onDeleteClick when clicking on the delete button', () => {
const clickHandler = jest.fn();
const mounted = mountHeader({
...defaultProps,
canDelete: true,
onDeleteClick: clickHandler,
});
expect(clickHandler).toHaveBeenCalledTimes(0);
mounted.find(`button[data-test-subj='savedObjectEditDelete']`).simulate('click');
expect(clickHandler).toHaveBeenCalledTimes(1);
});
it('only displays view button if canViewInApp is true', () => {
let mounted = mountHeader({
...defaultProps,
canViewInApp: true,
});
expect(mounted.exists(`a[data-test-subj='savedObjectEditViewInApp']`)).toBe(true);
mounted = mountHeader({
...defaultProps,
canViewInApp: false,
});
expect(mounted.exists(`a[data-test-subj='savedObjectEditViewInApp']`)).toBe(false);
});
});

View file

@ -0,0 +1,110 @@
/*
* 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 from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiTitle,
EuiButton,
EuiPageContentHeader,
EuiPageContentHeaderSection,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
interface HeaderProps {
canEdit: boolean;
canDelete: boolean;
canViewInApp: boolean;
type: string;
viewUrl: string;
onDeleteClick: () => void;
}
export const Header = ({
canEdit,
canDelete,
canViewInApp,
type,
viewUrl,
onDeleteClick,
}: HeaderProps) => {
return (
<EuiPageContentHeader>
<EuiPageContentHeaderSection>
<EuiTitle>
{canEdit ? (
<h1>
<FormattedMessage
id="kbn.management.objects.view.editItemTitle"
defaultMessage="Edit {title}"
values={{ title: type }}
/>
</h1>
) : (
<h1>
<FormattedMessage
id="kbn.management.objects.view.viewItemTitle"
defaultMessage="View {title}"
values={{ title: type }}
/>
</h1>
)}
</EuiTitle>
</EuiPageContentHeaderSection>
<EuiPageContentHeaderSection>
<EuiFlexGroup responsive={false}>
{canViewInApp && (
<EuiFlexItem grow={false}>
<EuiButton
size="s"
href={viewUrl}
iconType="eye"
data-test-subj="savedObjectEditViewInApp"
>
<FormattedMessage
id="kbn.management.objects.view.viewItemButtonLabel"
defaultMessage="View {title}"
values={{ title: type }}
/>
</EuiButton>
</EuiFlexItem>
)}
{canDelete && (
<EuiFlexItem grow={false}>
<EuiButton
color="danger"
size="s"
iconType="trash"
onClick={() => onDeleteClick()}
data-test-subj="savedObjectEditDelete"
>
<FormattedMessage
id="kbn.management.objects.view.deleteItemButtonLabel"
defaultMessage="Delete {title}"
values={{ title: type }}
/>
</EuiButton>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiPageContentHeaderSection>
</EuiPageContentHeader>
);
};

View file

@ -0,0 +1,23 @@
/*
* 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 { Header } from './header';
export { NotFoundErrors } from './not_found_errors';
export { Intro } from './intro';
export { Form } from './form';

View file

@ -0,0 +1,34 @@
/*
* 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 from 'react';
import { mount } from 'enzyme';
import { I18nProvider } from '@kbn/i18n/react';
import { Intro } from './intro';
describe('Intro component', () => {
it('renders correctly', () => {
const mounted = mount(
<I18nProvider>
<Intro />
</I18nProvider>
);
expect(mounted.find('Intro')).toMatchSnapshot();
});
});

View file

@ -0,0 +1,44 @@
/*
* 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 from 'react';
import { EuiCallOut } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
export const Intro = () => {
return (
<EuiCallOut
title={
<FormattedMessage
id="kbn.management.objects.view.howToModifyObjectTitle"
defaultMessage="Proceed with caution!"
/>
}
iconType="alert"
color="warning"
>
<div>
<FormattedMessage
id="kbn.management.objects.view.howToModifyObjectDescription"
defaultMessage="Modifying objects is for advanced users only. Object properties are not validated and invalid objects could cause errors, data loss, or worse. Unless someone with intimate knowledge of the code told you to be in here, you probably shouldn&rsquo;t be."
/>
</div>
</EuiCallOut>
);
};

View file

@ -0,0 +1,64 @@
/*
* 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 from 'react';
import { mount } from 'enzyme';
import { I18nProvider } from '@kbn/i18n/react';
import { NotFoundErrors } from './not_found_errors';
describe('NotFoundErrors component', () => {
const mountError = (type: string) =>
mount(
<I18nProvider>
<NotFoundErrors type={type} />
</I18nProvider>
).find('NotFoundErrors');
it('renders correctly for search type', () => {
const mounted = mountError('search');
expect(mounted).toMatchSnapshot();
expect(mounted.text()).toMatchInlineSnapshot(
`"There is a problem with this saved objectThe saved search associated with this object no longer exists.If you know what this error means, go ahead and fix it — otherwise click the delete button above."`
);
});
it('renders correctly for index-pattern type', () => {
const mounted = mountError('index-pattern');
expect(mounted).toMatchSnapshot();
expect(mounted.text()).toMatchInlineSnapshot(
`"There is a problem with this saved objectThe index pattern associated with this object no longer exists.If you know what this error means, go ahead and fix it — otherwise click the delete button above."`
);
});
it('renders correctly for index-pattern-field type', () => {
const mounted = mountError('index-pattern-field');
expect(mounted).toMatchSnapshot();
expect(mounted.text()).toMatchInlineSnapshot(
`"There is a problem with this saved objectA field associated with this object no longer exists in the index pattern.If you know what this error means, go ahead and fix it — otherwise click the delete button above."`
);
});
it('renders correctly for unknown type', () => {
const mounted = mountError('unknown');
expect(mounted).toMatchSnapshot();
expect(mounted.text()).toMatchInlineSnapshot(
`"There is a problem with this saved objectIf you know what this error means, go ahead and fix it — otherwise click the delete button above."`
);
});
});

View file

@ -0,0 +1,77 @@
/*
* 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 from 'react';
import { EuiCallOut } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
interface NotFoundErrors {
type: string;
}
export const NotFoundErrors = ({ type }: NotFoundErrors) => {
const getMessage = () => {
switch (type) {
case 'search':
return (
<FormattedMessage
id="kbn.management.objects.view.savedSearchDoesNotExistErrorMessage"
defaultMessage="The saved search associated with this object no longer exists."
/>
);
case 'index-pattern':
return (
<FormattedMessage
id="kbn.management.objects.view.indexPatternDoesNotExistErrorMessage"
defaultMessage="The index pattern associated with this object no longer exists."
/>
);
case 'index-pattern-field':
return (
<FormattedMessage
id="kbn.management.objects.view.fieldDoesNotExistErrorMessage"
defaultMessage="A field associated with this object no longer exists in the index pattern."
/>
);
default:
return null;
}
};
return (
<EuiCallOut
title={
<FormattedMessage
id="kbn.management.objects.view.savedObjectProblemErrorMessage"
defaultMessage="There is a problem with this saved object"
/>
}
iconType="alert"
color="danger"
>
<div>{getMessage()}</div>
<div>
<FormattedMessage
id="kbn.management.objects.view.howToFixErrorDescription"
defaultMessage="If you know what this error means, go ahead and fix it &mdash; otherwise click the delete button above."
/>
</div>
</EuiCallOut>
);
};

View file

@ -0,0 +1,132 @@
/*
* 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 { SimpleSavedObject, SavedObjectReference } from '../../../../../../../../core/public';
import { savedObjectsServiceMock } from '../../../../../../../../core/public/mocks';
import { createFieldList } from './create_field_list';
const savedObjectClientMock = savedObjectsServiceMock.createStartContract().client;
const createObject = <T>(
attributes: T,
references: SavedObjectReference[] = []
): SimpleSavedObject<T> =>
new SimpleSavedObject<T>(savedObjectClientMock, {
id: 'id',
type: 'type',
migrationVersion: {},
attributes,
references,
});
describe('createFieldList', () => {
it('generate fields based on the object attributes', () => {
const obj = createObject({
textField: 'some text',
numberField: 12,
boolField: true,
});
expect(createFieldList(obj)).toMatchInlineSnapshot(`
Array [
Object {
"name": "textField",
"type": "text",
"value": "some text",
},
Object {
"name": "numberField",
"type": "number",
"value": 12,
},
Object {
"name": "boolField",
"type": "boolean",
"value": true,
},
]
`);
});
it('detects json fields', () => {
const obj = createObject({
jsonField: `{"data": "value"}`,
});
expect(createFieldList(obj)).toMatchInlineSnapshot(`
Array [
Object {
"name": "jsonField",
"type": "json",
"value": "{
\\"data\\": \\"value\\"
}",
},
]
`);
});
it('handles array fields', () => {
const obj = createObject({
someArray: [1, 2, 3],
});
expect(createFieldList(obj)).toMatchInlineSnapshot(`
Array [
Object {
"name": "someArray",
"type": "array",
"value": "[
1,
2,
3
]",
},
]
`);
});
it('recursively collect nested fields', () => {
const obj = createObject({
firstLevel: {
firstLevelField: 'foo',
secondLevel: {
secondLevelFieldA: 'A',
secondLevelFieldB: 'B',
},
},
});
expect(createFieldList(obj)).toMatchInlineSnapshot(`
Array [
Object {
"name": "firstLevel.firstLevelField",
"type": "text",
"value": "foo",
},
Object {
"name": "firstLevel.secondLevel.secondLevelFieldA",
"type": "text",
"value": "A",
},
Object {
"name": "firstLevel.secondLevel.secondLevelFieldB",
"type": "text",
"value": "B",
},
]
`);
});
});

View file

@ -0,0 +1,135 @@
/*
* 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 { forOwn, indexBy, isNumber, isBoolean, isPlainObject, isString } from 'lodash';
import { SimpleSavedObject } from '../../../../../../../../core/public';
import { castEsToKbnFieldTypeName } from '../../../../../../../../plugins/data/public';
import { ObjectField } from '../types';
import { SavedObjectLoader } from '../../../../../../../../plugins/saved_objects/public';
const maxRecursiveIterations = 20;
export function createFieldList(
object: SimpleSavedObject,
service?: SavedObjectLoader
): ObjectField[] {
const fields = Object.entries(object.attributes as Record<string, any>).reduce(
(objFields, [key, value]) => {
return [...objFields, ...recursiveCreateFields(key, value)];
},
[] as ObjectField[]
);
if (service && (service as any).Class) {
addFieldsFromClass((service as any).Class, fields);
}
return fields;
}
/**
* Creates a field definition and pushes it to the memo stack. This function
* is designed to be used in conjunction with _.reduce(). If the
* values is plain object it will recurse through all the keys till it hits
* a string, number or an array.
*
* @param {string} key The key of the field
* @param {mixed} value The value of the field
* @param {array} parents The parent keys to the field
* @returns {array}
*/
const recursiveCreateFields = (key: string, value: any, parents: string[] = []): ObjectField[] => {
const path = [...parents, key];
if (path.length > maxRecursiveIterations) {
return [];
}
const field: ObjectField = { type: 'text', name: path.join('.'), value };
if (isString(field.value)) {
try {
field.value = JSON.stringify(JSON.parse(field.value), undefined, 2);
field.type = 'json';
} catch (err) {
field.type = 'text';
}
} else if (isNumber(field.value)) {
field.type = 'number';
} else if (Array.isArray(field.value)) {
field.type = 'array';
field.value = JSON.stringify(field.value, undefined, 2);
} else if (isBoolean(field.value)) {
field.type = 'boolean';
} else if (isPlainObject(field.value)) {
let fields: ObjectField[] = [];
forOwn(field.value, (childValue, childKey) => {
fields = [...fields, ...recursiveCreateFields(childKey as string, childValue, path)];
});
return fields;
}
return [field];
};
const addFieldsFromClass = function(
Class: { mapping: Record<string, string>; searchSource: any },
fields: ObjectField[]
) {
const fieldMap = indexBy(fields, 'name');
_.forOwn(Class.mapping, (esType, name) => {
if (!name || fieldMap[name]) {
return;
}
const getFieldTypeFromEsType = () => {
switch (castEsToKbnFieldTypeName(esType)) {
case 'string':
return 'text';
case 'number':
return 'number';
case 'boolean':
return 'boolean';
default:
return 'json';
}
};
fields.push({
name,
type: getFieldTypeFromEsType(),
value: undefined,
});
});
if (Class.searchSource && !fieldMap['kibanaSavedObjectMeta.searchSourceJSON']) {
fields.push({
name: 'kibanaSavedObjectMeta.searchSourceJSON',
type: 'json',
value: '{}',
});
}
if (!fieldMap.references) {
fields.push({
name: 'references',
type: 'array',
value: '[]',
});
}
};

View file

@ -17,22 +17,24 @@
* under the License.
*/
export function canViewInApp(uiCapabilities, type) {
import { Capabilities } from 'src/core/public';
export function canViewInApp(uiCapabilities: Capabilities, type: string): boolean {
switch (type) {
case 'search':
case 'searches':
return uiCapabilities.discover.show;
return uiCapabilities.discover.show as boolean;
case 'visualization':
case 'visualizations':
return uiCapabilities.visualize.show;
return uiCapabilities.visualize.show as boolean;
case 'index-pattern':
case 'index-patterns':
case 'indexPatterns':
return uiCapabilities.management.kibana.index_patterns;
return uiCapabilities.management.kibana.index_patterns as boolean;
case 'dashboard':
case 'dashboards':
return uiCapabilities.dashboard.show;
return uiCapabilities.dashboard.show as boolean;
default:
return uiCapabilities[type].show;
return uiCapabilities[type].show as boolean;
}
}

View file

@ -0,0 +1,176 @@
/*
* 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, { Component } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiSpacer, EuiPageContent } from '@elastic/eui';
import {
Capabilities,
SavedObjectsClientContract,
OverlayStart,
NotificationsStart,
SimpleSavedObject,
} from '../../../../../../../core/public';
import { ISavedObjectsManagementRegistry } from '../../saved_object_registry';
import { Header, NotFoundErrors, Intro, Form } from './components/object_view';
import { canViewInApp } from './lib/in_app_url';
import { SubmittedFormData } from './types';
interface SavedObjectEditionProps {
id: string;
serviceName: string;
serviceRegistry: ISavedObjectsManagementRegistry;
capabilities: Capabilities;
overlays: OverlayStart;
notifications: NotificationsStart;
notFoundType?: string;
savedObjectsClient: SavedObjectsClientContract;
}
interface SavedObjectEditionState {
type: string;
object?: SimpleSavedObject<any>;
}
export class SavedObjectEdition extends Component<
SavedObjectEditionProps,
SavedObjectEditionState
> {
constructor(props: SavedObjectEditionProps) {
super(props);
const { serviceRegistry, serviceName } = props;
const type = serviceRegistry.get(serviceName)!.service.type;
this.state = {
object: undefined,
type,
};
}
componentDidMount() {
const { id, savedObjectsClient } = this.props;
const { type } = this.state;
savedObjectsClient.get(type, id).then(object => {
this.setState({
object,
});
});
}
render() {
const {
capabilities,
notFoundType,
serviceRegistry,
id,
serviceName,
savedObjectsClient,
} = this.props;
const { type } = this.state;
const { object } = this.state;
const { edit: canEdit, delete: canDelete } = capabilities.savedObjectsManagement as Record<
string,
boolean
>;
const canView = canViewInApp(capabilities, type);
const service = serviceRegistry.get(serviceName)!.service;
return (
<EuiPageContent horizontalPosition="center" data-test-subj="savedObjectsEdit">
<Header
canEdit={canEdit}
canDelete={canDelete}
canViewInApp={canView}
type={type}
onDeleteClick={() => this.delete()}
viewUrl={service.urlFor(id)}
/>
{notFoundType && (
<>
<EuiSpacer size="s" />
<NotFoundErrors type={notFoundType} />
</>
)}
{canEdit && (
<>
<EuiSpacer size="s" />
<Intro />
</>
)}
{object && (
<>
<EuiSpacer size="m" />
<Form
object={object}
savedObjectsClient={savedObjectsClient}
service={service}
editionEnabled={canEdit}
onSave={this.saveChanges}
/>
</>
)}
</EuiPageContent>
);
}
async delete() {
const { id, savedObjectsClient, overlays, notifications } = this.props;
const { type, object } = this.state;
const confirmed = await overlays.openConfirm(
i18n.translate('kbn.management.objects.confirmModalOptions.modalDescription', {
defaultMessage: 'This action permanently removes the object from Kibana.',
}),
{
confirmButtonText: i18n.translate(
'kbn.management.objects.confirmModalOptions.deleteButtonLabel',
{
defaultMessage: 'Delete',
}
),
title: i18n.translate('kbn.management.objects.confirmModalOptions.modalTitle', {
defaultMessage: `Delete '{title}'?`,
values: {
title: object?.attributes?.title || 'saved Kibana object',
},
}),
buttonColor: 'danger',
}
);
if (confirmed) {
await savedObjectsClient.delete(type, id);
notifications.toasts.addSuccess(`Deleted '${object!.attributes.title}' ${type} object`);
this.redirectToListing();
}
}
saveChanges = async ({ attributes, references }: SubmittedFormData) => {
const { savedObjectsClient, notifications } = this.props;
const { object, type } = this.state;
await savedObjectsClient.update(object!.type, object!.id, attributes, { references });
notifications.toasts.addSuccess(`Updated '${attributes.title}' ${type} object`);
this.redirectToListing();
};
redirectToListing() {
window.location.hash = '/management/kibana/objects';
}
}

View file

@ -0,0 +1,38 @@
/*
* 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 { SavedObjectReference } from 'src/core/public';
export interface ObjectField {
type: FieldType;
name: string;
value: any;
}
export type FieldType = 'text' | 'number' | 'boolean' | 'array' | 'json';
export interface FieldState {
value?: any;
invalid?: boolean;
}
export interface SubmittedFormData {
attributes: any;
references: SavedObjectReference[];
}

View file

@ -0,0 +1,103 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
export default function({ getPageObjects, getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['common', 'settings']);
const setFieldValue = async (fieldName: string, value: string) => {
return testSubjects.setValue(`savedObjects-editField-${fieldName}`, value);
};
const getFieldValue = async (fieldName: string) => {
return testSubjects.getAttribute(`savedObjects-editField-${fieldName}`, 'value');
};
const focusAndClickButton = async (buttonSubject: string) => {
const button = await testSubjects.find(buttonSubject);
await button.scrollIntoViewIfNecessary();
await delay(10);
await button.focus();
await delay(10);
await button.click();
};
describe('TOTO saved objects edition page', () => {
beforeEach(async () => {
await esArchiver.load('saved_objects_management/edit_saved_object');
});
afterEach(async () => {
await esArchiver.unload('saved_objects_management/edit_saved_object');
});
it('allows to update the saved object when submitting', async () => {
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaSavedObjects();
let objects = await PageObjects.settings.getSavedObjectsInTable();
expect(objects.includes('A Dashboard')).to.be(true);
await PageObjects.common.navigateToActualUrl(
'kibana',
'/management/kibana/objects/savedDashboards/i-exist'
);
await testSubjects.existOrFail('savedObjectEditSave');
expect(await getFieldValue('title')).to.eql('A Dashboard');
await setFieldValue('title', 'Edited Dashboard');
await setFieldValue('description', 'Some description');
await focusAndClickButton('savedObjectEditSave');
objects = await PageObjects.settings.getSavedObjectsInTable();
expect(objects.includes('A Dashboard')).to.be(false);
expect(objects.includes('Edited Dashboard')).to.be(true);
await PageObjects.common.navigateToActualUrl(
'kibana',
'/management/kibana/objects/savedDashboards/i-exist'
);
expect(await getFieldValue('title')).to.eql('Edited Dashboard');
expect(await getFieldValue('description')).to.eql('Some description');
});
it('allows to delete a saved object', async () => {
await PageObjects.common.navigateToActualUrl(
'kibana',
'/management/kibana/objects/savedDashboards/i-exist'
);
await focusAndClickButton('savedObjectEditDelete');
await PageObjects.common.clickConfirmOnModal();
const objects = await PageObjects.settings.getSavedObjectsInTable();
expect(objects.includes('A Dashboard')).to.be(false);
});
});
}

View file

@ -0,0 +1,27 @@
/*
* 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 { FtrProviderContext } from '../../ftr_provider_context';
export default function savedObjectsManagementApp({ loadTestFile }: FtrProviderContext) {
describe('saved objects management', function savedObjectsManagementAppTestSuite() {
this.tags('ciGroup7');
loadTestFile(require.resolve('./edit_saved_object'));
});
}

View file

@ -32,6 +32,7 @@ export default async function({ readConfigFile }) {
require.resolve('./apps/discover'),
require.resolve('./apps/home'),
require.resolve('./apps/management'),
require.resolve('./apps/saved_objects_management'),
require.resolve('./apps/status_page'),
require.resolve('./apps/timelion'),
require.resolve('./apps/visualize'),

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,459 @@
{
"type": "index",
"value": {
"index": ".kibana",
"settings": {
"index": {
"number_of_shards": "1",
"auto_expand_replicas": "0-1",
"number_of_replicas": "0"
}
},
"mappings": {
"dynamic": "strict",
"properties": {
"apm-telemetry": {
"properties": {
"has_any_services": {
"type": "boolean"
},
"services_per_agent": {
"properties": {
"go": {
"type": "long",
"null_value": 0
},
"java": {
"type": "long",
"null_value": 0
},
"js-base": {
"type": "long",
"null_value": 0
},
"nodejs": {
"type": "long",
"null_value": 0
},
"python": {
"type": "long",
"null_value": 0
},
"ruby": {
"type": "long",
"null_value": 0
}
}
}
}
},
"canvas-workpad": {
"dynamic": "false",
"properties": {
"@created": {
"type": "date"
},
"@timestamp": {
"type": "date"
},
"id": {
"type": "text",
"index": false
},
"name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}
},
"config": {
"dynamic": "true",
"properties": {
"accessibility:disableAnimations": {
"type": "boolean"
},
"buildNum": {
"type": "keyword"
},
"dateFormat:tz": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"defaultIndex": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"telemetry:optIn": {
"type": "boolean"
}
}
},
"dashboard": {
"properties": {
"description": {
"type": "text"
},
"hits": {
"type": "integer"
},
"kibanaSavedObjectMeta": {
"properties": {
"searchSourceJSON": {
"type": "text"
}
}
},
"optionsJSON": {
"type": "text"
},
"panelsJSON": {
"type": "text"
},
"refreshInterval": {
"properties": {
"display": {
"type": "keyword"
},
"pause": {
"type": "boolean"
},
"section": {
"type": "integer"
},
"value": {
"type": "integer"
}
}
},
"timeFrom": {
"type": "keyword"
},
"timeRestore": {
"type": "boolean"
},
"timeTo": {
"type": "keyword"
},
"title": {
"type": "text"
},
"uiStateJSON": {
"type": "text"
},
"version": {
"type": "integer"
}
}
},
"map": {
"properties": {
"bounds": {
"type": "geo_shape",
"tree": "quadtree"
},
"description": {
"type": "text"
},
"layerListJSON": {
"type": "text"
},
"mapStateJSON": {
"type": "text"
},
"title": {
"type": "text"
},
"uiStateJSON": {
"type": "text"
},
"version": {
"type": "integer"
}
}
},
"graph-workspace": {
"properties": {
"description": {
"type": "text"
},
"kibanaSavedObjectMeta": {
"properties": {
"searchSourceJSON": {
"type": "text"
}
}
},
"numLinks": {
"type": "integer"
},
"numVertices": {
"type": "integer"
},
"title": {
"type": "text"
},
"version": {
"type": "integer"
},
"wsState": {
"type": "text"
}
}
},
"index-pattern": {
"properties": {
"fieldFormatMap": {
"type": "text"
},
"fields": {
"type": "text"
},
"intervalName": {
"type": "keyword"
},
"notExpandable": {
"type": "boolean"
},
"sourceFilters": {
"type": "text"
},
"timeFieldName": {
"type": "keyword"
},
"title": {
"type": "text"
},
"type": {
"type": "keyword"
},
"typeMeta": {
"type": "keyword"
}
}
},
"kql-telemetry": {
"properties": {
"optInCount": {
"type": "long"
},
"optOutCount": {
"type": "long"
}
}
},
"migrationVersion": {
"dynamic": "true",
"properties": {
"index-pattern": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"space": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
},
"namespace": {
"type": "keyword"
},
"search": {
"properties": {
"columns": {
"type": "keyword"
},
"description": {
"type": "text"
},
"hits": {
"type": "integer"
},
"kibanaSavedObjectMeta": {
"properties": {
"searchSourceJSON": {
"type": "text"
}
}
},
"sort": {
"type": "keyword"
},
"title": {
"type": "text"
},
"version": {
"type": "integer"
}
}
},
"server": {
"properties": {
"uuid": {
"type": "keyword"
}
}
},
"space": {
"properties": {
"_reserved": {
"type": "boolean"
},
"color": {
"type": "keyword"
},
"description": {
"type": "text"
},
"disabledFeatures": {
"type": "keyword"
},
"initials": {
"type": "keyword"
},
"name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 2048
}
}
}
}
},
"spaceId": {
"type": "keyword"
},
"telemetry": {
"properties": {
"enabled": {
"type": "boolean"
}
}
},
"timelion-sheet": {
"properties": {
"description": {
"type": "text"
},
"hits": {
"type": "integer"
},
"kibanaSavedObjectMeta": {
"properties": {
"searchSourceJSON": {
"type": "text"
}
}
},
"timelion_chart_height": {
"type": "integer"
},
"timelion_columns": {
"type": "integer"
},
"timelion_interval": {
"type": "keyword"
},
"timelion_other_interval": {
"type": "keyword"
},
"timelion_rows": {
"type": "integer"
},
"timelion_sheet": {
"type": "text"
},
"title": {
"type": "text"
},
"version": {
"type": "integer"
}
}
},
"type": {
"type": "keyword"
},
"updated_at": {
"type": "date"
},
"url": {
"properties": {
"accessCount": {
"type": "long"
},
"accessDate": {
"type": "date"
},
"createDate": {
"type": "date"
},
"url": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 2048
}
}
}
}
},
"visualization": {
"properties": {
"description": {
"type": "text"
},
"kibanaSavedObjectMeta": {
"properties": {
"searchSourceJSON": {
"type": "text"
}
}
},
"savedSearchId": {
"type": "keyword"
},
"title": {
"type": "text"
},
"uiStateJSON": {
"type": "text"
},
"version": {
"type": "integer"
},
"visState": {
"type": "text"
}
}
}
}
}
}
}

View file

@ -1340,7 +1340,7 @@
"kbn.management.landing.text": "すべてのツールの一覧は、左のメニューにあります。",
"kbn.management.objects.confirmModalOptions.deleteButtonLabel": "削除",
"kbn.management.objects.confirmModalOptions.modalDescription": "削除されたオブジェクトは復元できません",
"kbn.management.objects.confirmModalOptions.modalTitle": "保存された Kibana オブジェクトを削除しますか?",
"kbn.management.objects.confirmModalOptions.modalTitle": "{title} を削除しますか?",
"kbn.management.objects.deleteSavedObjectsConfirmModalDescription": "この操作は次の保存されたオブジェクトを削除します:",
"kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.cancelButtonLabel": "キャンセル",
"kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.deleteButtonLabel": "削除",

View file

@ -1340,7 +1340,7 @@
"kbn.management.landing.text": "在左侧菜单中可找到完整工具列表",
"kbn.management.objects.confirmModalOptions.deleteButtonLabel": "删除",
"kbn.management.objects.confirmModalOptions.modalDescription": "您无法恢复删除的对象",
"kbn.management.objects.confirmModalOptions.modalTitle": "删除已保存 Kibana 对象?",
"kbn.management.objects.confirmModalOptions.modalTitle": "删除 {title}?",
"kbn.management.objects.deleteSavedObjectsConfirmModalDescription": "此操作将删除以下已保存对象:",
"kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.cancelButtonLabel": "取消",
"kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.deleteButtonLabel": "删除",