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:
parent
6ed2918b6c
commit
395d621249
|
@ -69,6 +69,7 @@ export interface OverlayModalConfirmOptions {
|
|||
closeButtonAriaLabel?: string;
|
||||
'data-test-subj'?: string;
|
||||
defaultFocusedButton?: EuiConfirmModalProps['defaultFocusedButton'];
|
||||
buttonColor?: EuiConfirmModalProps['buttonColor'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 — 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’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>
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
`;
|
|
@ -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 shouldn’t 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 shouldn’t be.
|
||||
</FormattedMessage>
|
||||
</div>
|
||||
</div>
|
||||
</EuiText>
|
||||
</div>
|
||||
</EuiCallOut>
|
||||
</Intro>
|
||||
`;
|
|
@ -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>
|
||||
`;
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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}`;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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’t be."
|
||||
/>
|
||||
</div>
|
||||
</EuiCallOut>
|
||||
);
|
||||
};
|
|
@ -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."`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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 — otherwise click the delete button above."
|
||||
/>
|
||||
</div>
|
||||
</EuiCallOut>
|
||||
);
|
||||
};
|
|
@ -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",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -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: '[]',
|
||||
});
|
||||
}
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
}
|
|
@ -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[];
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
27
test/functional/apps/saved_objects_management/index.ts
Normal file
27
test/functional/apps/saved_objects_management/index.ts
Normal 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'));
|
||||
});
|
||||
}
|
|
@ -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
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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": "削除",
|
||||
|
|
|
@ -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": "删除",
|
||||
|
|
Loading…
Reference in a new issue