mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[ML] Removing old job wizard code (#48240)
* [ML] Removing old job wizard code * updating translations * adding autoload back in * updating translations
This commit is contained in:
parent
89860fb8fe
commit
53420a9f9d
188 changed files with 0 additions and 17103 deletions
|
@ -10,11 +10,8 @@ import 'uiExports/fieldFormats';
|
|||
import 'uiExports/savedObjectTypes';
|
||||
|
||||
import 'ui/courier';
|
||||
import 'ui/angular-bootstrap';
|
||||
import 'ui/autoload/all';
|
||||
|
||||
import 'plugins/ml/components/transition/transition';
|
||||
import 'plugins/ml/components/modal/modal';
|
||||
import 'plugins/ml/access_denied';
|
||||
import 'plugins/ml/jobs';
|
||||
import 'plugins/ml/overview';
|
||||
|
@ -24,10 +21,6 @@ import 'plugins/ml/data_frame_analytics';
|
|||
import 'plugins/ml/datavisualizer';
|
||||
import 'plugins/ml/explorer';
|
||||
import 'plugins/ml/timeseriesexplorer';
|
||||
import 'plugins/ml/components/form_label';
|
||||
import 'plugins/ml/components/json_tooltip';
|
||||
import 'plugins/ml/components/tooltip';
|
||||
import 'plugins/ml/components/confirm_modal';
|
||||
import 'plugins/ml/components/navigation_menu';
|
||||
import 'plugins/ml/components/loading_indicator';
|
||||
import 'plugins/ml/settings';
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import ngMock from 'ng_mock';
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
const mockModalInstance = { close: function () {}, dismiss: function () {} };
|
||||
|
||||
describe('ML - Confirm Modal Controller', () => {
|
||||
beforeEach(() => {
|
||||
ngMock.module('kibana');
|
||||
});
|
||||
|
||||
it('Initialize Confirm Modal Controller', (done) => {
|
||||
ngMock.inject(function ($rootScope, $controller) {
|
||||
const scope = $rootScope.$new();
|
||||
|
||||
expect(() => {
|
||||
$controller('MlConfirmModal', {
|
||||
$scope: scope,
|
||||
$modalInstance: mockModalInstance,
|
||||
params: {}
|
||||
});
|
||||
}).to.not.throwError();
|
||||
|
||||
expect(scope.okLabel).to.be('OK');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,25 +0,0 @@
|
|||
.confirm-modal {
|
||||
padding: $euiSizeL;
|
||||
cursor: auto;
|
||||
|
||||
// SASSTODO: Needs a proper selector
|
||||
h3 {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-weight: $euiFontWeightBold;
|
||||
padding-bottom: $euiSizeL;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 0px;
|
||||
padding-bottom: $euiSizeL;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 0px;
|
||||
padding-top: $euiSizeL;
|
||||
}
|
||||
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
@import 'confirm_modal';
|
|
@ -1,21 +0,0 @@
|
|||
<div class="confirm-modal">
|
||||
<div class="modal-title" ng-show="(title !== undefined && title !== '' )">{{title}}</div>
|
||||
<div class="modal-body" bind-html-unsafe="message"></div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
ng-click="ok()"
|
||||
ng-disabled="(saveLock === true)"
|
||||
class="kuiButton kuiButton--primary"
|
||||
aria-label="{{ ::'xpack.ml.confirmModal.okButtonAriaLabel' | i18n: {defaultMessage: 'Ok'} }}">
|
||||
{{okLabel}}
|
||||
</button>
|
||||
<button
|
||||
ng-hide="hideCancel"
|
||||
ng-click="cancel()"
|
||||
ng-disabled="(saveLock === true)"
|
||||
class="kuiButton kuiButton--primary"
|
||||
aria-label="{{ ::'xpack.ml.confirmModal.cancelButtonAriaLabel' | i18n: {defaultMessage: 'Cancel'} }}">
|
||||
{{cancelLabel}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
const module = uiModules.get('apps/ml');
|
||||
|
||||
module.controller('MlConfirmModal', function ($scope, $modalInstance, params) {
|
||||
|
||||
$scope.okFunc = params.ok;
|
||||
$scope.cancelFunc = params.cancel;
|
||||
|
||||
$scope.message = params.message || '';
|
||||
$scope.title = params.title || '';
|
||||
|
||||
$scope.okLabel = params.okLabel || i18n.translate('xpack.ml.confirmModal.okButtonLabel', {
|
||||
defaultMessage: 'OK',
|
||||
});
|
||||
|
||||
$scope.cancelLabel = params.cancelLabel || i18n.translate('xpack.ml.confirmModal.cancelButtonLabel', {
|
||||
defaultMessage: 'Cancel',
|
||||
});
|
||||
|
||||
$scope.hideCancel = params.hideCancel || false;
|
||||
|
||||
$scope.ok = function () {
|
||||
$scope.okFunc();
|
||||
$modalInstance.close();
|
||||
};
|
||||
|
||||
$scope.cancel = function () {
|
||||
$scope.cancelFunc();
|
||||
$modalInstance.close();
|
||||
};
|
||||
|
||||
});
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
// service for displaying a modal confirmation dialog with OK and Cancel buttons.
|
||||
|
||||
import template from './confirm_modal.html';
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml');
|
||||
|
||||
module.service('mlConfirmModalService', function ($modal) {
|
||||
|
||||
this.open = function (options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
$modal.open({
|
||||
template,
|
||||
controller: 'MlConfirmModal',
|
||||
backdrop: 'static',
|
||||
keyboard: false,
|
||||
size: (options.size === undefined) ? 'sm' : options.size,
|
||||
resolve: {
|
||||
params: function () {
|
||||
return {
|
||||
message: options.message,
|
||||
title: options.title,
|
||||
okLabel: options.okLabel,
|
||||
cancelLabel: options.cancelLabel,
|
||||
hideCancel: options.hideCancel,
|
||||
ok: resolve,
|
||||
cancel: reject,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import './confirm_modal_service';
|
||||
import './confirm_modal_controller';
|
|
@ -1,16 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DocumentationHelpLink renders the link 1`] = `
|
||||
<a
|
||||
className="documentation-help-link"
|
||||
href="http://fullUrl"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Label Text
|
||||
|
||||
<EuiIcon
|
||||
type="popout"
|
||||
/>
|
||||
</a>
|
||||
`;
|
|
@ -1,6 +0,0 @@
|
|||
// SASSTODO: Make a defined selector
|
||||
.documentation-help-link {
|
||||
i {
|
||||
margin-left: $euiSizeXS;
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
@import 'documentation_help_link';
|
|
@ -1,49 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { metadata } from 'ui/metadata';
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml');
|
||||
|
||||
import { DocumentationHelpLink } from './documentation_help_link_view';
|
||||
|
||||
module.directive('mlDocumentationHelpLink', function () {
|
||||
return {
|
||||
scope: {
|
||||
uri: '@mlUri',
|
||||
label: '@mlLabel'
|
||||
},
|
||||
restrict: 'AE',
|
||||
replace: true,
|
||||
link: function (scope, element) {
|
||||
const baseUrl = 'https://www.elastic.co';
|
||||
// metadata.branch corresponds to the version used in documentation links.
|
||||
const version = metadata.branch;
|
||||
|
||||
function renderReactComponent() {
|
||||
|
||||
const props = {
|
||||
fullUrl: `${baseUrl}/guide/en/elastic-stack-overview/${version}/${scope.uri}`,
|
||||
label: scope.label
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(DocumentationHelpLink, props),
|
||||
element[0]
|
||||
);
|
||||
}
|
||||
|
||||
scope.$watch('uri', renderReactComponent);
|
||||
}
|
||||
};
|
||||
|
||||
});
|
|
@ -1,26 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { EuiIcon } from '@elastic/eui';
|
||||
|
||||
export function DocumentationHelpLink({ fullUrl, label }) {
|
||||
return (
|
||||
<a
|
||||
href={fullUrl}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
className="documentation-help-link"
|
||||
>
|
||||
{label} <EuiIcon type="popout" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
DocumentationHelpLink.propTypes = {
|
||||
fullUrl: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired
|
||||
};
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { DocumentationHelpLink } from './documentation_help_link_view';
|
||||
|
||||
describe('DocumentationHelpLink', () => {
|
||||
const props = {
|
||||
fullUrl: 'http://fullUrl',
|
||||
label: 'Label Text'
|
||||
};
|
||||
|
||||
const component = (
|
||||
<DocumentationHelpLink {...props} />
|
||||
);
|
||||
|
||||
const wrapper = shallow(component);
|
||||
|
||||
test('renders the link', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -1,7 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import './documentation_help_link';
|
|
@ -1,69 +0,0 @@
|
|||
ml-form-filter-input {
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
line-height: 0;
|
||||
|
||||
// SASSTODO: Make a real selector
|
||||
input[type="text"] {
|
||||
background-color: transparent;
|
||||
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
padding-right: $euiSizeXL;
|
||||
}
|
||||
|
||||
// SASSTODO: This likely should no be removed
|
||||
input[type="text"]::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ml-filter-input-left-icon {
|
||||
padding-left: $euiSizeXL;
|
||||
}
|
||||
|
||||
// SASSTODO: Rewrite this selector completely
|
||||
.fa.fa-search {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 8px;
|
||||
color: $euiColorLightShade;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-top: 10px;
|
||||
margin-left: 0px;
|
||||
font-size: 1.2em;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
// SASSTODO: Rewrite this selector completely
|
||||
.ml-filter-progress-icon {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
color: $euiColorMediumShade;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-top: 10px;
|
||||
margin-right: 0px;
|
||||
|
||||
a {
|
||||
color: $euiColorLightShade;
|
||||
|
||||
.fa.fa-times-circle {
|
||||
font-size: 1.4em;
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: $euiColorMediumShade;
|
||||
}
|
||||
|
||||
.fa.fa-spinner {
|
||||
font-size: 1.2em;
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
@import 'form_filter_input';
|
|
@ -1,21 +0,0 @@
|
|||
<div class="fa fa-search"></div>
|
||||
<input
|
||||
class="form-control ml-filter-input-left-icon"
|
||||
type="text"
|
||||
aria-label="{{ariaLabel}}"
|
||||
placeholder="{{placeholder}}"
|
||||
ng-model="filter"
|
||||
ng-change="filterChanged()" />
|
||||
<div class="ml-filter-progress-icon">
|
||||
<div ng-show="filterIcon===1">
|
||||
<i class='fa fa-spinner fa-spin'></i>
|
||||
</div>
|
||||
<div ng-show="filterIcon===0">
|
||||
<a
|
||||
aria-label="{{ ::'xpack.ml.formFilterInput.clearFilterAriaLabel' | i18n: {defaultMessage: 'Clear filter'} }}"
|
||||
ng-click="clearFilter()"
|
||||
>
|
||||
<i class="fa fa-times-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import template from './form_filter_input.html';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import angular from 'angular';
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml');
|
||||
|
||||
module.directive('mlFormFilterInput', function () {
|
||||
return {
|
||||
scope: {
|
||||
placeholder: '@?',
|
||||
filter: '=',
|
||||
filterIcon: '=',
|
||||
filterChanged: '=',
|
||||
clearFilter: '='
|
||||
},
|
||||
restrict: 'E',
|
||||
replace: false,
|
||||
template,
|
||||
link(scope) {
|
||||
const placeholderIsDefined = angular.isDefined(scope.placeholder);
|
||||
|
||||
scope.placeholder = placeholderIsDefined
|
||||
? scope.placeholder
|
||||
: i18n.translate('xpack.ml.formFilterInput.filterPlaceholder', { defaultMessage: 'Filter' });
|
||||
|
||||
scope.ariaLabel = placeholderIsDefined
|
||||
? scope.placeholder
|
||||
: i18n.translate('xpack.ml.formFilterInput.filterAriaLabel', { defaultMessage: 'Filter' });
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,9 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import './form_filter_input_directive';
|
|
@ -1,28 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FormLabel Basic initialization 1`] = `
|
||||
<Fragment>
|
||||
<label
|
||||
className="euiFormLabel"
|
||||
id="ml_aria_label_undefined"
|
||||
/>
|
||||
<JsonTooltip
|
||||
position="top"
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`FormLabel Full initialization 1`] = `
|
||||
<Fragment>
|
||||
<label
|
||||
className="euiFormLabel"
|
||||
id="ml_aria_label_uid"
|
||||
>
|
||||
Label Text
|
||||
</label>
|
||||
<JsonTooltip
|
||||
id="uid"
|
||||
position="top"
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import ngMock from 'ng_mock';
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
describe('ML - <ml-form-label>', () => {
|
||||
let $scope;
|
||||
let $compile;
|
||||
let $element;
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(() => {
|
||||
ngMock.inject(function ($injector) {
|
||||
$compile = $injector.get('$compile');
|
||||
const $rootScope = $injector.get('$rootScope');
|
||||
$scope = $rootScope.$new();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
$scope.$destroy();
|
||||
});
|
||||
|
||||
it('Basic initialization', () => {
|
||||
$element = $compile('<ml-form-label />')($scope);
|
||||
const scope = $element.isolateScope();
|
||||
scope.$digest();
|
||||
|
||||
expect(scope.labelId).to.be.an('undefined');
|
||||
expect($element.find('label').text()).to.be('');
|
||||
});
|
||||
|
||||
it('Full initialization', () => {
|
||||
const labelId = 'uid';
|
||||
const labelText = 'Label Text';
|
||||
$element = $compile(`
|
||||
<ml-form-label label-id="${labelId}">${labelText}</ml-form-label>
|
||||
`)($scope);
|
||||
const scope = $element.isolateScope();
|
||||
scope.$digest();
|
||||
|
||||
const labelElement = $element.find('label');
|
||||
expect(labelElement[0].attributes.id.value).to.be('ml_aria_label_' + labelId);
|
||||
expect(labelElement.text()).to.be(labelText);
|
||||
});
|
||||
|
||||
});
|
|
@ -1,14 +0,0 @@
|
|||
ml-form-label {
|
||||
display: inline-flex;
|
||||
|
||||
// SASSTODO: Apply a real selector
|
||||
span[ml-info-icon] {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
// SASSTODO: Apply a real selector
|
||||
span[ml-info-icon],
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
@import 'form_label';
|
|
@ -1,47 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { JsonTooltip } from '../json_tooltip/json_tooltip';
|
||||
|
||||
// Component for creating a form label including a hoverable icon
|
||||
// to provide additional information in a tooltip. Label and tooltip
|
||||
// text elements get unique ids based on label-id so they can be
|
||||
// referenced by attributes, for example:
|
||||
//
|
||||
// <FormLabel labelId="uid">Label Text</FormLabel>
|
||||
// <input
|
||||
// type="text"
|
||||
// aria-labelledby="ml_aria_label_uid"
|
||||
// aria-describedby="ml_aria_description_uid"
|
||||
// />
|
||||
//
|
||||
// Writing this as a class based component because stateless components
|
||||
// cannot use ref(). Once angular is completely gone this can be rewritten
|
||||
// as a function stateless component.
|
||||
export class FormLabel extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.labelRef = React.createRef();
|
||||
}
|
||||
render() {
|
||||
// labelClassName is used so we can override the class with 'kuiFormLabel'
|
||||
// when used in an angular context. Once the component is no longer used from
|
||||
// within angular, this prop can be removed and the className can be hardcoded.
|
||||
const { labelId, labelClassName = 'euiFormLabel', children } = this.props;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<label className={labelClassName} id={`ml_aria_label_${labelId}`} ref={this.labelRef}>{children}</label>
|
||||
<JsonTooltip id={labelId} position="top" />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
FormLabel.propTypes = {
|
||||
labelId: PropTypes.string
|
||||
};
|
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { FormLabel } from './form_label';
|
||||
|
||||
describe('FormLabel', () => {
|
||||
|
||||
test('Basic initialization', () => {
|
||||
const wrapper = shallow(<FormLabel />);
|
||||
const props = wrapper.props();
|
||||
expect(props.labelId).toBeUndefined();
|
||||
expect(wrapper.find('label').text()).toBe('');
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Full initialization', () => {
|
||||
const labelId = 'uid';
|
||||
const labelText = 'Label Text';
|
||||
const wrapper = shallow(<FormLabel labelId={labelId}>{labelText}</FormLabel>);
|
||||
|
||||
const labelElement = wrapper.find('label');
|
||||
expect(labelElement.props().id).toBe(`ml_aria_label_${labelId}`);
|
||||
expect(labelElement.text()).toBe(labelText);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -1,52 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import angular from 'angular';
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml', ['react']);
|
||||
|
||||
import { FormLabel } from './form_label';
|
||||
|
||||
// directive for creating a form label including a hoverable icon
|
||||
// to provide additional information in a tooltip. label and tooltip
|
||||
// text elements get unique ids based on label-id so they can be
|
||||
// referenced by attributes, for example:
|
||||
//
|
||||
// <ml-form-label label-id="uid">Label Text</ml-form-label>
|
||||
// <input
|
||||
// type="text"
|
||||
// aria-labelledby="ml_aria_label_uid"
|
||||
// aria-describedby="ml_aria_description_uid"
|
||||
// />
|
||||
module.directive('mlFormLabel', function () {
|
||||
return {
|
||||
scope: {
|
||||
labelId: '@'
|
||||
},
|
||||
restrict: 'E',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
link: (scope, element, attrs, ctrl, transclude) => {
|
||||
const props = {
|
||||
labelId: scope.labelId,
|
||||
labelClassName: 'kuiFormLabel',
|
||||
// transclude the label text/elements from the angular template
|
||||
// to the labelRef from the react component.
|
||||
ref: c => angular.element(c.labelRef.current).append(transclude())
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(FormLabel, props),
|
||||
element[0]
|
||||
);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,9 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import './form_label_directive';
|
|
@ -1 +0,0 @@
|
|||
@import 'job_group_select';
|
|
@ -1,33 +0,0 @@
|
|||
// SASSTODO: More specific selector, this is a bad cascade
|
||||
.ml-job-group-select {
|
||||
.ui-select-container {
|
||||
.ui-select-choices-group-label {
|
||||
color: $euiColorMediumShade;
|
||||
}
|
||||
|
||||
// SASSTODO: More specific selector
|
||||
small {
|
||||
font-size: 12px;
|
||||
margin-top: 2px;
|
||||
font-style: italic;
|
||||
color: $euiColorMediumShade;
|
||||
}
|
||||
|
||||
.ui-select-choices-row.active {
|
||||
// SASSTODO: More specific selector
|
||||
small {
|
||||
color: $euiColorEmptyShade;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ui-select-multiple.ui-select-bootstrap {
|
||||
// SASSTODO: Needs proper variables
|
||||
padding: 3px 5px 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// SASSTODO: More specific selector, this is a dangerous overwrite
|
||||
body > .ui-select-bootstrap.open {
|
||||
z-index: 1050;
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import './job_group_select';
|
|
@ -1,33 +0,0 @@
|
|||
<div class="ml-job-group-select">
|
||||
<ui-select
|
||||
ng-model="mlGroupSelect.selectedGroups"
|
||||
on-select="mlGroupSelect.onGroupsChanged()"
|
||||
on-remove="mlGroupSelect.onGroupsChanged()"
|
||||
ng-disabled="mlGroupSelect.disabled"
|
||||
multiple
|
||||
tagging='mlGroupSelect.createNewItem'
|
||||
append-to-body=true
|
||||
>
|
||||
<ui-select-match
|
||||
placeholder="{{:: 'xpack.ml.jobGroupSelect.jobGroupPlaceholder' | i18n: { defaultMessage: 'Job Group' } }}"
|
||||
>
|
||||
{{$item.id}}
|
||||
</ui-select-match>
|
||||
<ui-select-choices
|
||||
repeat="group in mlGroupSelect.groups | filter: { id: $select.search }"
|
||||
group-by="mlGroupSelect.groupTypes"
|
||||
>
|
||||
<div ng-if="group.isTag" class="select-item" ng-bind-html="(group.id | highlight: $select.search) +' <small>' + newGroupLabel + '</small>'"></div>
|
||||
<div ng-if="!group.isTag" class="select-item" >
|
||||
<div ng-bind-html="group.id | highlight: $select.search"></div>
|
||||
<small
|
||||
i18n-id="xpack.ml.jobGroupSelect.otherJobsInGroupLabel"
|
||||
i18n-default-message="Other jobs in this group: {groupCount}"
|
||||
i18n-values="{
|
||||
groupCount: group.count,
|
||||
}"
|
||||
></small>
|
||||
</div>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
</div>
|
|
@ -1,122 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import template from './job_group_select.html';
|
||||
|
||||
import { mlJobService } from 'plugins/ml/services/job_service';
|
||||
import { mlCalendarService } from 'plugins/ml/services/calendar_service';
|
||||
import { InitAfterBindingsWorkaround } from 'ui/compat';
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml');
|
||||
|
||||
module.directive('mlJobGroupSelect', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template,
|
||||
scope: {
|
||||
jobGroups: '=',
|
||||
disabled: '=',
|
||||
externalUpdateFunction: '='
|
||||
},
|
||||
controllerAs: 'mlGroupSelect',
|
||||
bindToController: true,
|
||||
controller: class MlGroupSelectController extends InitAfterBindingsWorkaround {
|
||||
|
||||
initAfterBindings($scope) {
|
||||
this.$scope = $scope;
|
||||
this.selectedGroups = [];
|
||||
this.groups = [];
|
||||
this.$scope.newGroupLabel = i18n.translate('xpack.ml.jobGroupSelect.newGroupLabel', { defaultMessage: '(new group)' });
|
||||
|
||||
// load the jobs, in case they've not been loaded before
|
||||
// in order to get the job groups
|
||||
mlJobService.loadJobs()
|
||||
.then(() => {
|
||||
// temp id map for fast deduplication
|
||||
const tempGroupIds = {};
|
||||
|
||||
const jobGroups = mlJobService.getJobGroups();
|
||||
this.groups = jobGroups.map((g) => {
|
||||
tempGroupIds[g.id] = null;
|
||||
return { id: g.id, count: g.jobs.length, isTag: false };
|
||||
});
|
||||
// if jobGroups hasn't been passed in or it isn't an array, create a new one
|
||||
// needed because advanced job configuration page may not have a jobs array. e.g. when cloning
|
||||
if (Array.isArray(this.jobGroups) === false) {
|
||||
this.jobGroups = [];
|
||||
}
|
||||
|
||||
// load the calendar groups and add any additional groups to the list
|
||||
mlCalendarService.loadCalendars(mlJobService.jobs)
|
||||
.then(() => {
|
||||
const calendarGroups = mlCalendarService.getCalendarGroups();
|
||||
calendarGroups.forEach((g) => {
|
||||
// if the group is not used in any jobs, add it to the list
|
||||
if (tempGroupIds[g.id] === undefined) {
|
||||
this.groups.push({ id: g.id, count: 0, isTag: false });
|
||||
}
|
||||
});
|
||||
this.populateSelectedGroups(this.jobGroups);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('Could not load groups from calendars', error);
|
||||
this.populateSelectedGroups(this.jobGroups);
|
||||
})
|
||||
.then(() => {
|
||||
$scope.$applyAsync();
|
||||
});
|
||||
});
|
||||
|
||||
// make the populateSelectedGroups function callable from elsewhere.
|
||||
// this is used in the advanced job configuration page, when the user has edited the
|
||||
// job's JSON, we need to force update the displayed selected groups
|
||||
if (this.externalUpdateFunction !== undefined) {
|
||||
this.externalUpdateFunction.update = (groups) => { this.populateSelectedGroups(groups); };
|
||||
}
|
||||
}
|
||||
|
||||
// takes a list of groups ids
|
||||
// if the ids has already been used, add it to list of selected groups for display
|
||||
// if it hasn't, create the group
|
||||
populateSelectedGroups(groups) {
|
||||
this.selectedGroups = [];
|
||||
groups.forEach(gId => {
|
||||
const tempGroup = _.filter(this.groups, { id: gId });
|
||||
if (tempGroup.length) {
|
||||
this.selectedGroups.push(tempGroup[0]);
|
||||
} else {
|
||||
this.selectedGroups.push(this.createNewItem(gId));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onGroupsChanged() {
|
||||
// wipe the groups and add all of the selected ids
|
||||
this.jobGroups.length = 0;
|
||||
this.selectedGroups.forEach((group) => {
|
||||
this.jobGroups.push(group.id);
|
||||
});
|
||||
}
|
||||
|
||||
createNewItem(groupId) {
|
||||
const gId = groupId.toLowerCase();
|
||||
return ({ id: gId, count: 0, isTag: true });
|
||||
}
|
||||
|
||||
groupTypes(group) {
|
||||
if(group.isTag === false) {
|
||||
return i18n.translate('xpack.ml.jobGroupSelect.existingGroupsLabel', { defaultMessage: 'Existing groups' });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,48 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`JsonTooltip Initialization with a non-existing tooltip attribute doesn't throw an error 1`] = `
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="ml-info-icon"
|
||||
>
|
||||
<EuiIconTip
|
||||
content=""
|
||||
/>
|
||||
<span
|
||||
className="ml-info-tooltip-text"
|
||||
id="ml_aria_description_non_existing_attribute"
|
||||
/>
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`JsonTooltip Initialize with existing tooltip attribute 1`] = `
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="ml-info-icon"
|
||||
>
|
||||
<EuiIconTip
|
||||
content="Unique identifier for job, can use lowercase alphanumeric and underscores."
|
||||
/>
|
||||
<span
|
||||
className="ml-info-tooltip-text"
|
||||
id="ml_aria_description_new_job_id"
|
||||
>
|
||||
Unique identifier for job, can use lowercase alphanumeric and underscores.
|
||||
</span>
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`JsonTooltip Plain initialization doesn't throw an error 1`] = `
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="ml-info-icon"
|
||||
>
|
||||
<EuiIconTip
|
||||
content=""
|
||||
/>
|
||||
<span
|
||||
className="ml-info-tooltip-text"
|
||||
id="ml_aria_description_undefined"
|
||||
/>
|
||||
</span>
|
||||
`;
|
|
@ -1,66 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import ngMock from 'ng_mock';
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { getTooltips } from '../tooltips';
|
||||
|
||||
describe('ML - <ml-info-icon>', () => {
|
||||
let $scope;
|
||||
let $compile;
|
||||
let $element;
|
||||
let tooltips;
|
||||
|
||||
before(() => {
|
||||
tooltips = getTooltips();
|
||||
});
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(() => {
|
||||
ngMock.inject(function ($injector) {
|
||||
$compile = $injector.get('$compile');
|
||||
const $rootScope = $injector.get('$rootScope');
|
||||
$scope = $rootScope.$new();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
$scope.$destroy();
|
||||
});
|
||||
|
||||
it('Plain initialization doesn\'t throw an error', () => {
|
||||
$element = $compile('<ml-info-icon />')($scope);
|
||||
const scope = $element.isolateScope();
|
||||
|
||||
expect(scope.id).to.be.an('undefined');
|
||||
});
|
||||
|
||||
it('Initialization with a non-existing tooltip attribute doesn\'t throw an error', () => {
|
||||
const id = 'non_existing_attribute';
|
||||
$element = $compile(`<i ml-info-icon="${id}" />`)($scope);
|
||||
const scope = $element.isolateScope();
|
||||
scope.$digest();
|
||||
|
||||
expect(scope.id).to.be(id);
|
||||
});
|
||||
|
||||
it('Initialize with existing tooltip attribute', () => {
|
||||
const id = 'new_job_id';
|
||||
$element = $compile(`<i ml-info-icon="${id}" />`)($scope);
|
||||
const scope = $element.isolateScope();
|
||||
scope.$digest();
|
||||
|
||||
// test scope values
|
||||
expect(scope.id).to.be(id);
|
||||
|
||||
// test the rendered span element which should be referenced by aria-describedby
|
||||
const span = $element.find('span.ml-info-tooltip-text');
|
||||
expect(span[0].id).to.be('ml_aria_description_' + id);
|
||||
expect(span.text()).to.be(tooltips[id].text);
|
||||
});
|
||||
|
||||
});
|
|
@ -1 +0,0 @@
|
|||
@import 'json_tooltip';
|
|
@ -1,21 +0,0 @@
|
|||
.ml-info-icon {
|
||||
color: $euiColorMediumShade;
|
||||
margin: 0 $euiSizeXS;
|
||||
transition: color 0.15s; // SASSTODO: Variablize
|
||||
|
||||
// SASSTODO: This needs to be removed
|
||||
/* hard-coded euiIcon size because EuiIconTip doesn't pass on the size attribute to EuiIcon */
|
||||
.euiIcon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.ml-info-tooltip-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ml-info-icon:hover {
|
||||
color: $euiColorDarkestShade;
|
||||
transition: color 0.15s 0.15s; // SASSTODO: Variablize
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import './json_tooltip_directive';
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
// component for placing an icon with a popover tooltip anywhere on a page
|
||||
// the id will match an entry in tooltips.json
|
||||
import { getTooltips } from './tooltips';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import { EuiIconTip } from '@elastic/eui';
|
||||
|
||||
export const JsonTooltip = ({ id, position }) => {
|
||||
const tooltips = getTooltips();
|
||||
const text = (tooltips[id]) ? tooltips[id].text : '';
|
||||
return (
|
||||
<span aria-hidden="true" className="ml-info-icon">
|
||||
<EuiIconTip
|
||||
content={text}
|
||||
position={position}
|
||||
/>
|
||||
<span id={`ml_aria_description_${id}`} className="ml-info-tooltip-text">{text}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
JsonTooltip.propTypes = {
|
||||
id: PropTypes.string,
|
||||
position: PropTypes.string
|
||||
};
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { JsonTooltip } from './json_tooltip';
|
||||
import { getTooltips } from './tooltips';
|
||||
|
||||
describe('JsonTooltip', () => {
|
||||
let tooltips;
|
||||
beforeAll(() => {
|
||||
tooltips = getTooltips();
|
||||
});
|
||||
|
||||
test(`Plain initialization doesn't throw an error`, () => {
|
||||
const wrapper = shallow(<JsonTooltip />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test(`Initialization with a non-existing tooltip attribute doesn't throw an error`, () => {
|
||||
const id = 'non_existing_attribute';
|
||||
const wrapper = shallow(<JsonTooltip id={id} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Initialize with existing tooltip attribute', () => {
|
||||
const id = 'new_job_id';
|
||||
const wrapper = shallow(<JsonTooltip id={id} />);
|
||||
|
||||
// test the rendered span element which should be referenced by aria-describedby
|
||||
const span = wrapper.find('span.ml-info-tooltip-text');
|
||||
expect(span.props().id).toBe(`ml_aria_description_${id}`);
|
||||
expect(span.text()).toBe(tooltips[id].text);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml', ['react']);
|
||||
|
||||
import { JsonTooltip } from './json_tooltip';
|
||||
|
||||
// directive for placing an i icon with a popover tooltip anywhere on a page
|
||||
// tooltip format: <i ml-info-icon="<the_id>" />
|
||||
// the_id will match an entry in tooltips.json
|
||||
module.directive('mlInfoIcon', function () {
|
||||
return {
|
||||
scope: {
|
||||
id: '@mlInfoIcon',
|
||||
position: '@'
|
||||
},
|
||||
restrict: 'AE',
|
||||
replace: false,
|
||||
link: (scope, element) => {
|
||||
const props = {
|
||||
id: scope.id,
|
||||
position: scope.position
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(JsonTooltip, props),
|
||||
element[0]
|
||||
);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,316 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
let tooltips;
|
||||
|
||||
export const getTooltips = () => {
|
||||
if (tooltips) {
|
||||
return tooltips;
|
||||
}
|
||||
|
||||
return tooltips = {
|
||||
new_job_id: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobIdTooltip', {
|
||||
defaultMessage: 'Unique identifier for job, can use lowercase alphanumeric and underscores.'
|
||||
})
|
||||
},
|
||||
new_job_description: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobDescriptionTooltip', {
|
||||
defaultMessage: 'Optional descriptive text.'
|
||||
})
|
||||
},
|
||||
new_job_group: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobGroupTooltip', {
|
||||
defaultMessage: 'Optional grouping for jobs. New groups can be created or picked from the list of existing groups.'
|
||||
})
|
||||
},
|
||||
new_job_custom_urls: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobCustomUrlsTooltip', {
|
||||
defaultMessage:
|
||||
'Optional drill-through links to source data. Supports string substitution for analyzed fields e.g. {hostnameParam}.',
|
||||
values: { hostnameParam: '$hostname$' }
|
||||
})
|
||||
},
|
||||
new_job_bucketspan: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobBucketSpanTooltip', {
|
||||
defaultMessage: 'Interval for time series analysis.'
|
||||
})
|
||||
},
|
||||
new_job_sparsedata: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobSparseDataTooltip', {
|
||||
defaultMessage: 'Check if you wish to ignore empty buckets from being considered anomalous.'
|
||||
})
|
||||
},
|
||||
new_job_summarycountfieldname: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobSummaryCountFieldNameTooltip', {
|
||||
defaultMessage: 'Optional, for use if input data has been pre-summarized e.g. {docCountParam}.',
|
||||
values: { docCountParam: 'doc_count' }
|
||||
})
|
||||
},
|
||||
new_job_categorizationfieldname: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobCategorizationFieldNameTooltip', {
|
||||
defaultMessage: 'Optional, for use if analyzing unstructured log data. Using text data types is recommended.'
|
||||
})
|
||||
},
|
||||
new_job_categorizationfilters: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobCategorizationFiltersTooltip', {
|
||||
defaultMessage: 'Optional, apply regular expressions to the categorization field'
|
||||
})
|
||||
},
|
||||
new_job_detectors: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobDetectorsTooltip', {
|
||||
defaultMessage: 'Defines the fields and functions used for analysis.'
|
||||
})
|
||||
},
|
||||
new_job_influencers: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobInfluencersTooltip', {
|
||||
defaultMessage: 'Select which categorical fields have influence on the results. ' +
|
||||
'Who/what might you "blame" for an anomaly? Recommend 1-3 influencers.'
|
||||
})
|
||||
},
|
||||
new_job_detector_description: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobDetectorDescriptionTooltip', {
|
||||
defaultMessage: 'User-friendly text used for dashboards.'
|
||||
})
|
||||
},
|
||||
new_job_detector_function: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobDetectorFunctionTooltip', {
|
||||
defaultMessage: 'Analysis functions to be performed e.g. sum, count.'
|
||||
})
|
||||
},
|
||||
new_job_detector_fieldname: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobDetectorFieldNameTooltip', {
|
||||
defaultMessage: 'Required for functions: sum, mean, median, max, min, info_content, distinct_count.'
|
||||
})
|
||||
},
|
||||
new_job_detector_fieldname_subset: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobDetectorFieldNameSubsetTooltip', {
|
||||
defaultMessage: 'Required for functions: sum, mean, max, min, distinct_count.'
|
||||
})
|
||||
},
|
||||
new_job_detector_byfieldname: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobDetectorByFieldNameTooltip', {
|
||||
defaultMessage: `Required for individual analysis where anomalies are detected compared to an entity's own past behavior.`
|
||||
})
|
||||
},
|
||||
new_job_detector_overfieldname: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobDetectorOverFieldNameTooltip', {
|
||||
defaultMessage: 'Required for population analysis where anomalies are detected compared to the behavior of the population.'
|
||||
})
|
||||
},
|
||||
new_job_detector_partitionfieldname: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobDetectorPartitionFieldNameTooltip', {
|
||||
defaultMessage: 'Allows segmentation of modeling into logical groups.'
|
||||
})
|
||||
},
|
||||
new_job_detector_excludefrequent: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobDetectorExcludeFrequentTooltip', {
|
||||
defaultMessage: 'If true will automatically identify and exclude frequently occurring entities which ' +
|
||||
'may otherwise have dominated results.'
|
||||
})
|
||||
},
|
||||
new_job_data_format: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobDataFormatTooltip', {
|
||||
defaultMessage: 'Describes the format of the input data: delimited, JSON, single line or Elasticsearch.'
|
||||
})
|
||||
},
|
||||
new_job_time_field: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobTimeFieldTooltip', {
|
||||
defaultMessage: 'Name of the field containing the timestamp.'
|
||||
})
|
||||
},
|
||||
new_job_time_format: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobTimeFormatTooltip', {
|
||||
defaultMessage: 'Format of the time field: epoch, epoch_ms or Java DateTimeFormatter string. Important to get right.'
|
||||
})
|
||||
},
|
||||
new_job_delimiter: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobDelimiterTooltip', {
|
||||
defaultMessage: 'Character used to separate fields.'
|
||||
})
|
||||
},
|
||||
new_job_quote_character: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobQuoteCharacterTooltip', {
|
||||
defaultMessage: 'Character used to encapsulate values containing reserved characters.'
|
||||
})
|
||||
},
|
||||
new_job_enable_datafeed_job: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobEnableDatafeedJobTooltip', {
|
||||
defaultMessage: 'Required for jobs that analyze data from Elasticsearch.'
|
||||
})
|
||||
},
|
||||
new_job_data_source: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobDataSourceTooltip', {
|
||||
defaultMessage: 'Elasticsearch versions 1.7.x and 2+ supported.'
|
||||
})
|
||||
},
|
||||
new_job_datafeed_query: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobDatafeedQueryTooltip', {
|
||||
defaultMessage: 'Elasticsearch Query DSL for filtering input data.'
|
||||
})
|
||||
},
|
||||
new_job_datafeed_query_delay: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobDatafeedQueryDelayTooltip', {
|
||||
defaultMessage: 'Advanced option. Time delay in seconds, between current time and latest input data time.'
|
||||
})
|
||||
},
|
||||
new_job_datafeed_frequency: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobDatafeedFrequencyTooltip', {
|
||||
defaultMessage: 'Advanced option. The interval between searches.'
|
||||
})
|
||||
},
|
||||
new_job_datafeed_scrollsize: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobDatafeedScrollSizeTooltip', {
|
||||
defaultMessage: 'Advanced option. The maximum number of documents requested for a search.'
|
||||
})
|
||||
},
|
||||
new_job_data_preview: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobDataPreviewTooltip', {
|
||||
defaultMessage: 'This preview returns the contents of the source field only.'
|
||||
})
|
||||
},
|
||||
new_job_elasticsearch_server: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobElasticsearchServerTooltip', {
|
||||
defaultMessage: 'Server address and port of Elasticsearch source.'
|
||||
})
|
||||
},
|
||||
new_job_enable_authenticated: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobEnableAuthenticatedTooltip', {
|
||||
defaultMessage: 'Select to specify username and password for secure access.'
|
||||
})
|
||||
},
|
||||
new_job_datafeed_retrieve_source: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobDatafeedRetrieveSourceTooltip', {
|
||||
defaultMessage: 'Advanced option. Select to retrieve unfiltered _source document, instead of specified fields.'
|
||||
})
|
||||
},
|
||||
new_job_enable_model_plot: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobEnableModelPlotTooltip', {
|
||||
defaultMessage: 'Select to enable model plot. Stores model information along with results. ' +
|
||||
'Can add considerable overhead to the performance of the system.'
|
||||
})
|
||||
},
|
||||
new_job_model_memory_limit: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobModelMemoryLimitTooltip', {
|
||||
defaultMessage: 'An approximate limit for the amount of memory used by the analytical models.'
|
||||
})
|
||||
},
|
||||
new_filter_ruleaction: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newFilterRuleActionTooltip', {
|
||||
defaultMessage: `A string specifying the rule action. Initially, the only valid option is 'filter_results' but it ` +
|
||||
`provisions for expansion to actions like 'disable_modeling'.`
|
||||
})
|
||||
},
|
||||
new_filter_targetfieldname: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newFilterTargetFieldNameTooltip', {
|
||||
defaultMessage: 'A string expecting a field name. The filter will apply on all results for the targetFieldName ' +
|
||||
'value the ruleConditions apply. When empty, filtering applies only to results for which the ruleConditions apply.'
|
||||
})
|
||||
},
|
||||
new_action_targetfieldvalue: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newActionTargetFieldValueTooltip', {
|
||||
defaultMessage: 'A string expecting a value for targetFieldName. If any of the ruleConditions apply, all results ' +
|
||||
'will be excluded for that particular targetValue but not for others. Can only be specified if targetFieldName is not empty.'
|
||||
})
|
||||
},
|
||||
new_action_conditionsconnective: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newActionConditionsConnectiveTooltip', {
|
||||
defaultMessage: 'The logical connective of the ruleConditions.'
|
||||
})
|
||||
},
|
||||
new_action_ruleconditions: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newActionRuleConditionsTooltip', {
|
||||
defaultMessage: 'The list of conditions used to apply the rules.'
|
||||
})
|
||||
},
|
||||
new_action_conditiontype: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newActionConditionTypeTooltip', {
|
||||
defaultMessage: 'A string specifying the condition type.'
|
||||
})
|
||||
},
|
||||
new_action_fieldname: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newActionFieldNameTooltip', {
|
||||
defaultMessage: 'A string specifying the field name on which the rule applies. When empty, rule applies to all results.'
|
||||
})
|
||||
},
|
||||
new_action_fieldvalue: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newActionFieldValueTooltip', {
|
||||
defaultMessage: 'A string specifying the numerical field value on which the rule applies. ' +
|
||||
'When empty, rule applies to all values of fieldName. Can only be specified if fieldName is not empty.'
|
||||
})
|
||||
},
|
||||
new_action_condition: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newActionConditionTooltip', {
|
||||
defaultMessage: 'The condition comparing fieldValue and value.'
|
||||
})
|
||||
},
|
||||
new_action_value: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newActionValueTooltip', {
|
||||
defaultMessage: 'The numerical value to compare against fieldValue.'
|
||||
})
|
||||
},
|
||||
new_action_valuelist: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newActionValueListTooltip', {
|
||||
defaultMessage: 'A string that is a unique identifier to a list. Only applicable and required when conditionType is categorical.'
|
||||
})
|
||||
},
|
||||
forecasting_modal_run_duration: {
|
||||
text: i18n.translate('xpack.ml.tooltips.forecastingModalRunDurationTooltip', {
|
||||
defaultMessage: 'Length of forecast, up to a maximum of 3650 days. ' +
|
||||
'Use s for seconds, m for minutes, h for hours, d for days, w for weeks.'
|
||||
})
|
||||
},
|
||||
forecasting_modal_view_list: {
|
||||
text: i18n.translate('xpack.ml.tooltips.forecastingModalViewListTooltip', {
|
||||
defaultMessage: 'Lists a maximum of five of the most recently run forecasts.'
|
||||
})
|
||||
},
|
||||
new_job_recognizer_job_prefix: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newJobRecognizerJobPrefixTooltip', {
|
||||
defaultMessage: 'A prefix which will be added to the beginning of each job ID.'
|
||||
})
|
||||
},
|
||||
new_custom_url_label: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newCustomUrlLabelTooltip', {
|
||||
defaultMessage: 'A label for the drill-through link.'
|
||||
})
|
||||
},
|
||||
new_custom_url_link_to: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newCustomUrlLinkToTooltip', {
|
||||
defaultMessage: 'Link to a Kibana dashboard, Discover or another URL.'
|
||||
})
|
||||
},
|
||||
new_custom_url_dashboard: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newCustomUrlDashboardTooltip', {
|
||||
defaultMessage: 'The dashboard to link to.'
|
||||
})
|
||||
},
|
||||
new_custom_url_discover_index: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newCustomUrlDiscoverIndexTooltip', {
|
||||
defaultMessage: 'The index pattern to view in Discover.'
|
||||
})
|
||||
},
|
||||
new_custom_url_query_entity: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newCustomUrlQueryEntityTooltip', {
|
||||
defaultMessage: 'Optional, entities from the anomaly that will be used in the dashboard query.'
|
||||
})
|
||||
},
|
||||
new_custom_url_value: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newCustomUrlValueTooltip', {
|
||||
defaultMessage: 'URL of the drill-through link. Supports string substitution for analyzed fields e.g. {hostnameParam}.',
|
||||
values: { hostnameParam: '$hostname$' }
|
||||
})
|
||||
},
|
||||
new_custom_url_time_range: {
|
||||
text: i18n.translate('xpack.ml.tooltips.newCustomUrlTimeRangeTooltip', {
|
||||
defaultMessage: 'The time span that will be displayed in the drill-down page. ' +
|
||||
'Set automatically, or enter a specific interval e.g. 10m or 1h.'
|
||||
})
|
||||
}
|
||||
};
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
<div class="modal-backdrop fade {{ backdropClass }}"
|
||||
ng-class="{in: animate}"
|
||||
ng-style="{'z-index': 1040 + (index && 1 || 0) + index*10}"
|
||||
></div>
|
|
@ -1,429 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import angular from 'angular';
|
||||
import backdropTemplate from './backdrop.html';
|
||||
import modalTemplate from './window.html';
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml');
|
||||
|
||||
module
|
||||
/**
|
||||
* A helper, internal data structure that acts as a map but also allows getting / removing
|
||||
* elements in the LIFO order
|
||||
*/
|
||||
.factory('$$stackedMap', function () {
|
||||
return {
|
||||
createNew: function () {
|
||||
const stack = [];
|
||||
|
||||
return {
|
||||
add: function (key, value) {
|
||||
stack.push({
|
||||
key: key,
|
||||
value: value
|
||||
});
|
||||
},
|
||||
get: function (key) {
|
||||
for (let i = 0; i < stack.length; i++) {
|
||||
if (key === stack[i].key) {
|
||||
return stack[i];
|
||||
}
|
||||
}
|
||||
},
|
||||
keys: function () {
|
||||
const keys = [];
|
||||
for (let i = 0; i < stack.length; i++) {
|
||||
keys.push(stack[i].key);
|
||||
}
|
||||
return keys;
|
||||
},
|
||||
top: function () {
|
||||
return stack[stack.length - 1];
|
||||
},
|
||||
remove: function (key) {
|
||||
let idx = -1;
|
||||
for (let i = 0; i < stack.length; i++) {
|
||||
if (key === stack[i].key) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return stack.splice(idx, 1)[0];
|
||||
},
|
||||
removeTop: function () {
|
||||
return stack.splice(stack.length - 1, 1)[0];
|
||||
},
|
||||
length: function () {
|
||||
return stack.length;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
})
|
||||
|
||||
/**
|
||||
* A helper directive for the $modal service. It creates a backdrop element.
|
||||
*/
|
||||
.directive('modalBackdrop', ['$timeout', function ($timeout) {
|
||||
return {
|
||||
restrict: 'EA',
|
||||
replace: true,
|
||||
template: backdropTemplate,
|
||||
link: function (scope, element, attrs) {
|
||||
scope.backdropClass = attrs.backdropClass || '';
|
||||
|
||||
scope.animate = false;
|
||||
|
||||
//trigger CSS transitions
|
||||
$timeout(function () {
|
||||
scope.animate = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
}])
|
||||
|
||||
.directive('modalWindow', ['$modalStack', '$timeout', function ($modalStack, $timeout) {
|
||||
return {
|
||||
restrict: 'EA',
|
||||
scope: {
|
||||
index: '@',
|
||||
animate: '='
|
||||
},
|
||||
replace: true,
|
||||
transclude: true,
|
||||
template: modalTemplate,
|
||||
// templateUrl: function (tElement, tAttrs) {
|
||||
// return tAttrs.templateUrl || 'template/modal/window.html';
|
||||
// },
|
||||
link: function (scope, element, attrs) {
|
||||
element.addClass(attrs.windowClass || '');
|
||||
scope.size = attrs.size;
|
||||
|
||||
$timeout(function () {
|
||||
// trigger CSS transitions
|
||||
scope.animate = true;
|
||||
|
||||
/**
|
||||
* Auto-focusing of a freshly-opened modal element causes any child elements
|
||||
* with the autofocus attribute to lose focus. This is an issue on touch
|
||||
* based devices which will show and then hide the onscreen keyboard.
|
||||
* Attempts to refocus the autofocus element via JavaScript will not reopen
|
||||
* the onscreen keyboard. Fixed by updated the focusing logic to only autofocus
|
||||
* the modal element if the modal does not contain an autofocus element.
|
||||
*/
|
||||
if (!element[0].querySelectorAll('[autofocus]').length) {
|
||||
element[0].focus();
|
||||
}
|
||||
});
|
||||
|
||||
scope.close = function (evt) {
|
||||
const modal = $modalStack.getTop();
|
||||
if (modal && modal.value.backdrop && modal.value.backdrop !== 'static' && (evt.target === evt.currentTarget)) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
$modalStack.dismiss(modal.key, 'backdrop click');
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}])
|
||||
|
||||
.directive('modalTransclude', function () {
|
||||
return {
|
||||
link: function ($scope, $element, $attrs, controller, $transclude) {
|
||||
$transclude($scope.$parent, function (clone) {
|
||||
$element.empty();
|
||||
$element.append(clone);
|
||||
});
|
||||
}
|
||||
};
|
||||
})
|
||||
|
||||
.factory('$modalStack', ['$transition', '$timeout', '$document', '$compile', '$rootScope', '$$stackedMap',
|
||||
function ($transition, $timeout, $document, $compile, $rootScope, $$stackedMap) {
|
||||
|
||||
const OPENED_MODAL_CLASS = 'modal-open';
|
||||
|
||||
let backdropDomEl;
|
||||
let backdropScope;
|
||||
const openedWindows = $$stackedMap.createNew();
|
||||
const $modalStack = {};
|
||||
|
||||
function backdropIndex() {
|
||||
let topBackdropIndex = -1;
|
||||
const opened = openedWindows.keys();
|
||||
for (let i = 0; i < opened.length; i++) {
|
||||
if (openedWindows.get(opened[i]).value.backdrop) {
|
||||
topBackdropIndex = i;
|
||||
}
|
||||
}
|
||||
return topBackdropIndex;
|
||||
}
|
||||
|
||||
$rootScope.$watch(backdropIndex, function (newBackdropIndex) {
|
||||
if (backdropScope) {
|
||||
backdropScope.index = newBackdropIndex;
|
||||
}
|
||||
});
|
||||
|
||||
function removeModalWindow(modalInstance) {
|
||||
|
||||
const body = $document.find('body').eq(0);
|
||||
const modalWindow = openedWindows.get(modalInstance).value;
|
||||
|
||||
//clean up the stack
|
||||
openedWindows.remove(modalInstance);
|
||||
|
||||
//remove window DOM element
|
||||
removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, 300, function () {
|
||||
modalWindow.modalScope.$destroy();
|
||||
body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0);
|
||||
checkRemoveBackdrop();
|
||||
});
|
||||
}
|
||||
|
||||
function checkRemoveBackdrop() {
|
||||
//remove backdrop if no longer needed
|
||||
if (backdropDomEl && backdropIndex() === -1) {
|
||||
let backdropScopeRef = backdropScope;
|
||||
removeAfterAnimate(backdropDomEl, backdropScope, 150, function () {
|
||||
backdropScopeRef.$destroy();
|
||||
backdropScopeRef = null;
|
||||
});
|
||||
backdropDomEl = undefined;
|
||||
backdropScope = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function removeAfterAnimate(domEl, scope, emulateTime, done) {
|
||||
// Closing animation
|
||||
scope.animate = false;
|
||||
|
||||
const transitionEndEventName = $transition.transitionEndEventName;
|
||||
if (transitionEndEventName) {
|
||||
// transition out
|
||||
const timeout = $timeout(afterAnimating, emulateTime);
|
||||
|
||||
domEl.bind(transitionEndEventName, function () {
|
||||
$timeout.cancel(timeout);
|
||||
afterAnimating();
|
||||
scope.$apply();
|
||||
});
|
||||
} else {
|
||||
// Ensure this call is async
|
||||
$timeout(afterAnimating);
|
||||
}
|
||||
|
||||
function afterAnimating() {
|
||||
if (afterAnimating.done) {
|
||||
return;
|
||||
}
|
||||
afterAnimating.done = true;
|
||||
|
||||
domEl.remove();
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$document.bind('keydown', function (evt) {
|
||||
let modal;
|
||||
|
||||
if (evt.which === 27) {
|
||||
modal = openedWindows.top();
|
||||
if (modal && modal.value.keyboard) {
|
||||
evt.preventDefault();
|
||||
$rootScope.$apply(function () {
|
||||
$modalStack.dismiss(modal.key, 'escape key press');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$modalStack.open = function (modalInstance, modal) {
|
||||
|
||||
openedWindows.add(modalInstance, {
|
||||
deferred: modal.deferred,
|
||||
modalScope: modal.scope,
|
||||
backdrop: modal.backdrop,
|
||||
keyboard: modal.keyboard
|
||||
});
|
||||
|
||||
const body = $document.find('body').eq(0);
|
||||
const currBackdropIndex = backdropIndex();
|
||||
|
||||
if (currBackdropIndex >= 0 && !backdropDomEl) {
|
||||
backdropScope = $rootScope.$new(true);
|
||||
backdropScope.index = currBackdropIndex;
|
||||
const angularBackgroundDomEl = angular.element('<div modal-backdrop></div>');
|
||||
angularBackgroundDomEl.attr('backdrop-class', modal.backdropClass);
|
||||
backdropDomEl = $compile(angularBackgroundDomEl)(backdropScope);
|
||||
body.append(backdropDomEl);
|
||||
}
|
||||
|
||||
const angularDomEl = angular.element('<div modal-window></div>');
|
||||
angularDomEl.attr({
|
||||
'template-url': modal.windowTemplateUrl,
|
||||
'window-class': modal.windowClass,
|
||||
'size': modal.size,
|
||||
'index': openedWindows.length() - 1,
|
||||
'animate': 'animate'
|
||||
}).html(modal.content);
|
||||
|
||||
const modalDomEl = $compile(angularDomEl)(modal.scope);
|
||||
openedWindows.top().value.modalDomEl = modalDomEl;
|
||||
body.append(modalDomEl);
|
||||
body.addClass(OPENED_MODAL_CLASS);
|
||||
};
|
||||
|
||||
$modalStack.close = function (modalInstance, result) {
|
||||
const modalWindow = openedWindows.get(modalInstance);
|
||||
if (modalWindow) {
|
||||
modalWindow.value.deferred.resolve(result);
|
||||
removeModalWindow(modalInstance);
|
||||
}
|
||||
};
|
||||
|
||||
$modalStack.dismiss = function (modalInstance, reason) {
|
||||
const modalWindow = openedWindows.get(modalInstance);
|
||||
if (modalWindow) {
|
||||
modalWindow.value.deferred.reject(reason);
|
||||
removeModalWindow(modalInstance);
|
||||
}
|
||||
};
|
||||
|
||||
$modalStack.dismissAll = function (reason) {
|
||||
let topModal = this.getTop();
|
||||
while (topModal) {
|
||||
this.dismiss(topModal.key, reason);
|
||||
topModal = this.getTop();
|
||||
}
|
||||
};
|
||||
|
||||
$modalStack.getTop = function () {
|
||||
return openedWindows.top();
|
||||
};
|
||||
|
||||
return $modalStack;
|
||||
}])
|
||||
|
||||
.provider('$modal', function () {
|
||||
const $modalProvider = {
|
||||
options: {
|
||||
backdrop: true, //can be also false or 'static'
|
||||
keyboard: true
|
||||
},
|
||||
$get: ['$injector', '$rootScope', '$q', '$http', '$templateCache', '$controller', '$modalStack',
|
||||
function ($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack) {
|
||||
|
||||
const $modal = {};
|
||||
|
||||
function getTemplatePromise(options) {
|
||||
return options.template ? $q.when(options.template) :
|
||||
$http.get(angular.isFunction(options.templateUrl) ? (options.templateUrl)() : options.templateUrl,
|
||||
{ cache: $templateCache }).then(function (result) {
|
||||
return result.data;
|
||||
});
|
||||
}
|
||||
|
||||
function getResolvePromises(resolves) {
|
||||
const promisesArr = [];
|
||||
angular.forEach(resolves, function (value) {
|
||||
if (angular.isFunction(value) || angular.isArray(value)) {
|
||||
promisesArr.push($q.when($injector.invoke(value)));
|
||||
}
|
||||
});
|
||||
return promisesArr;
|
||||
}
|
||||
|
||||
$modal.open = function (modalOptions) {
|
||||
|
||||
const modalResultDeferred = $q.defer();
|
||||
const modalOpenedDeferred = $q.defer();
|
||||
|
||||
//prepare an instance of a modal to be injected into controllers and returned to a caller
|
||||
const modalInstance = {
|
||||
result: modalResultDeferred.promise,
|
||||
opened: modalOpenedDeferred.promise,
|
||||
close: function (result) {
|
||||
$modalStack.close(modalInstance, result);
|
||||
},
|
||||
dismiss: function (reason) {
|
||||
$modalStack.dismiss(modalInstance, reason);
|
||||
}
|
||||
};
|
||||
|
||||
//merge and clean up options
|
||||
modalOptions = angular.extend({}, $modalProvider.options, modalOptions);
|
||||
modalOptions.resolve = modalOptions.resolve || {};
|
||||
|
||||
//verify options
|
||||
if (!modalOptions.template && !modalOptions.templateUrl) {
|
||||
throw new Error('One of template or templateUrl options is required.');
|
||||
}
|
||||
|
||||
const templateAndResolvePromise =
|
||||
$q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve)));
|
||||
|
||||
|
||||
templateAndResolvePromise.then(function resolveSuccess(tplAndVars) {
|
||||
|
||||
const modalScope = (modalOptions.scope || $rootScope).$new();
|
||||
modalScope.$close = modalInstance.close;
|
||||
modalScope.$dismiss = modalInstance.dismiss;
|
||||
|
||||
let ctrlInstance;
|
||||
const ctrlLocals = {};
|
||||
let resolveIter = 1;
|
||||
|
||||
//controllers
|
||||
if (modalOptions.controller) {
|
||||
ctrlLocals.$scope = modalScope;
|
||||
ctrlLocals.$modalInstance = modalInstance;
|
||||
angular.forEach(modalOptions.resolve, function (value, key) {
|
||||
ctrlLocals[key] = tplAndVars[resolveIter++];
|
||||
});
|
||||
|
||||
ctrlInstance = $controller(modalOptions.controller, ctrlLocals);
|
||||
if (modalOptions.controllerAs) {
|
||||
modalScope[modalOptions.controllerAs] = ctrlInstance;
|
||||
}
|
||||
}
|
||||
|
||||
$modalStack.open(modalInstance, {
|
||||
scope: modalScope,
|
||||
deferred: modalResultDeferred,
|
||||
content: tplAndVars[0],
|
||||
backdrop: modalOptions.backdrop,
|
||||
keyboard: modalOptions.keyboard,
|
||||
backdropClass: modalOptions.backdropClass,
|
||||
windowClass: modalOptions.windowClass,
|
||||
windowTemplateUrl: modalOptions.windowTemplateUrl,
|
||||
size: modalOptions.size
|
||||
});
|
||||
|
||||
}, function resolveError(reason) {
|
||||
modalResultDeferred.reject(reason);
|
||||
});
|
||||
|
||||
templateAndResolvePromise.then(function () {
|
||||
modalOpenedDeferred.resolve(true);
|
||||
}, function () {
|
||||
modalOpenedDeferred.reject(false);
|
||||
});
|
||||
|
||||
return modalInstance;
|
||||
};
|
||||
|
||||
return $modal;
|
||||
}]
|
||||
};
|
||||
|
||||
return $modalProvider;
|
||||
});
|
|
@ -1,3 +0,0 @@
|
|||
<div tabindex="-1" role="dialog" class="modal fade" ng-class="{in: animate}" ng-style="{'z-index': 1050 + index*10, display: 'block'}" ng-click="close($event)">
|
||||
<div class="modal-dialog" ng-class="{'modal-sm': size == 'sm', 'modal-lg': size == 'lg'}"><div class="modal-content" modal-transclude></div></div>
|
||||
</div>
|
|
@ -1,7 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import './tooltip_directive';
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml', ['react']);
|
||||
|
||||
import { Tooltip } from './tooltip_view';
|
||||
|
||||
module.directive('mlTooltip', function ($compile) {
|
||||
const link = function (scope, element) {
|
||||
const content = element.html();
|
||||
element.html('');
|
||||
|
||||
const props = {
|
||||
position: scope.position,
|
||||
text: scope.text,
|
||||
transclude: (el) => {
|
||||
const transcludeScope = scope.$new();
|
||||
const compiled = $compile(content)(transcludeScope);
|
||||
el.append(compiled[0]);
|
||||
}
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(Tooltip, props),
|
||||
element[0]
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
replace: true,
|
||||
scope: false,
|
||||
transclude: false,
|
||||
link
|
||||
};
|
||||
});
|
|
@ -1,26 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
EuiToolTip
|
||||
} from '@elastic/eui';
|
||||
|
||||
export function Tooltip({ position = 'top', text, transclude }) {
|
||||
return (
|
||||
<EuiToolTip position={position} content={text}>
|
||||
<span ref={transclude} />
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
Tooltip.propTypes = {
|
||||
position: PropTypes.string,
|
||||
text: PropTypes.string,
|
||||
transclude: PropTypes.func
|
||||
};
|
|
@ -1,90 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import angular from 'angular';
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml');
|
||||
|
||||
module
|
||||
/**
|
||||
* $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete.
|
||||
* @param {DOMElement} element The DOMElement that will be animated.
|
||||
* @param {string|object|function} trigger The thing that will cause the transition to start:
|
||||
* - As a string, it represents the css class to be added to the element.
|
||||
* - As an object, it represents a hash of style attributes to be applied to the element.
|
||||
* - As a function, it represents a function to be called that will cause the transition to occur.
|
||||
* @return {Promise} A promise that is resolved when the transition finishes.
|
||||
*/
|
||||
.factory('$transition', ['$q', '$timeout', '$rootScope', function ($q, $timeout, $rootScope) {
|
||||
const $transition = function (element, trigger, options) {
|
||||
options = options || {};
|
||||
const deferred = $q.defer();
|
||||
const endEventName = $transition[options.animation ? 'animationEndEventName' : 'transitionEndEventName'];
|
||||
|
||||
const transitionEndHandler = function () {
|
||||
$rootScope.$apply(function () {
|
||||
element.unbind(endEventName, transitionEndHandler);
|
||||
deferred.resolve(element);
|
||||
});
|
||||
};
|
||||
|
||||
if (endEventName) {
|
||||
element.bind(endEventName, transitionEndHandler);
|
||||
}
|
||||
|
||||
// Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur
|
||||
$timeout(function () {
|
||||
if (angular.isString(trigger)) {
|
||||
element.addClass(trigger);
|
||||
} else if (angular.isFunction(trigger)) {
|
||||
trigger(element);
|
||||
} else if (angular.isObject(trigger)) {
|
||||
element.css(trigger);
|
||||
}
|
||||
//If browser does not support transitions, instantly resolve
|
||||
if (!endEventName) {
|
||||
deferred.resolve(element);
|
||||
}
|
||||
});
|
||||
|
||||
// Add our custom cancel function to the promise that is returned
|
||||
// We can call this if we are about to run a new transition, which we know will prevent this transition from ending,
|
||||
// i.e. it will therefore never raise a transitionEnd event for that transition
|
||||
deferred.promise.cancel = function () {
|
||||
if (endEventName) {
|
||||
element.unbind(endEventName, transitionEndHandler);
|
||||
}
|
||||
deferred.reject('Transition cancelled');
|
||||
};
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
// Work out the name of the transitionEnd event
|
||||
const transElement = document.createElement('trans');
|
||||
const transitionEndEventNames = {
|
||||
'WebkitTransition': 'webkitTransitionEnd',
|
||||
'MozTransition': 'transitionend',
|
||||
'OTransition': 'oTransitionEnd',
|
||||
'transition': 'transitionend'
|
||||
};
|
||||
const animationEndEventNames = {
|
||||
'WebkitTransition': 'webkitAnimationEnd',
|
||||
'MozTransition': 'animationend',
|
||||
'OTransition': 'oAnimationEnd',
|
||||
'transition': 'animationend'
|
||||
};
|
||||
function findEndEventName(endEventNames) {
|
||||
for (const name in endEventNames) {
|
||||
if (transElement.style[name] !== undefined) {
|
||||
return endEventNames[name];
|
||||
}
|
||||
}
|
||||
}
|
||||
$transition.transitionEndEventName = findEndEventName(transitionEndEventNames);
|
||||
$transition.animationEndEventName = findEndEventName(animationEndEventNames);
|
||||
return $transition;
|
||||
}]);
|
|
@ -33,18 +33,13 @@
|
|||
@import 'components/annotations/annotation_description_list/index'; // SASSTODO: This file overwrites EUI directly
|
||||
@import 'components/anomalies_table/index'; // SASSTODO: This file overwrites EUI directly
|
||||
@import 'components/chart_tooltip/index';
|
||||
@import 'components/confirm_modal/index';
|
||||
@import 'components/controls/index';
|
||||
@import 'components/documentation_help_link/index';
|
||||
@import 'components/entity_cell/index';
|
||||
@import 'components/field_title_bar/index';
|
||||
@import 'components/field_type_icon/index';
|
||||
@import 'components/form_filter_input/index'; // SASSTODO: This file needs to be rewritten
|
||||
@import 'components/form_label/index';
|
||||
@import 'components/influencers_list/index';
|
||||
@import 'components/items_grid/index';
|
||||
@import 'components/job_selector/index';
|
||||
@import 'components/json_tooltip/index'; // SASSTODO: This file overwrites EUI directly
|
||||
@import 'components/loading_indicator/index'; // SASSTODO: This component should be replaced with EuiLoadingSpinner
|
||||
@import 'components/messagebar/index';
|
||||
@import 'components/navigation_menu/index';
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
@import 'components/custom_url_editor/index';
|
||||
@import 'jobs_list/index'; // SASSTODO: Various EUI overwrites throughout this folder
|
||||
@import 'new_job/index'; // SASSTODO: Lots of files need rewrites in here
|
||||
|
|
|
@ -7,9 +7,4 @@
|
|||
|
||||
|
||||
import './jobs_list';
|
||||
import './new_job/advanced';
|
||||
import './new_job/simple/single_metric';
|
||||
import './new_job/simple/multi_metric';
|
||||
import './new_job/simple/population';
|
||||
import 'plugins/ml/components/validate_job';
|
||||
import './new_job_new';
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
@import 'advanced/index';
|
||||
@import 'simple/index';
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
import ngMock from 'ng_mock';
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
describe('ML - Advanced Job Wizard - New Job Controller', () => {
|
||||
beforeEach(() => {
|
||||
ngMock.module('kibana');
|
||||
});
|
||||
|
||||
it('Initialize New Job Controller', (done) => {
|
||||
ngMock.inject(function ($rootScope, $controller, $route) {
|
||||
// Set up the $route current props required for the tests.
|
||||
$route.current = {
|
||||
locals: {
|
||||
indexPattern: {},
|
||||
savedSearch: {}
|
||||
}
|
||||
};
|
||||
|
||||
const scope = $rootScope.$new();
|
||||
|
||||
expect(() => {
|
||||
$controller('MlNewJob', { $scope: scope });
|
||||
}).to.not.throwError();
|
||||
|
||||
// This is just about initializing the controller and making sure
|
||||
// all angularjs based dependencies get loaded without error.
|
||||
// This simple scope test is just a final sanity check.
|
||||
expect(scope.ui.pageTitle).to.be('Create a new job');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,206 +0,0 @@
|
|||
.ml-new-job {
|
||||
display: block;
|
||||
}
|
||||
// Required to prevent overflow of flex item in IE11
|
||||
.ml-new-job-callout {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// SASSTODO: Proper calcs. This looks too brittle to touch quickly
|
||||
.detector {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
background-color: $euiColorLightestShade;
|
||||
padding: 10px;
|
||||
padding-right: 60px;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 3px;
|
||||
border: $euiBorderThin;
|
||||
min-width: 360px;
|
||||
|
||||
& > .button-container {
|
||||
position: absolute;
|
||||
top: $euiSizeS;
|
||||
right: $euiSizeS;
|
||||
}
|
||||
|
||||
.filter-list {
|
||||
border-bottom: $euiBorderThin;
|
||||
margin: $euiSizeXS 0px;
|
||||
|
||||
// SASSTODO: Proper calcs
|
||||
.filter {
|
||||
height: 22px;
|
||||
margin: $euiSizeXS 0px;
|
||||
font-style: italic;
|
||||
|
||||
.button-container {
|
||||
float: right;
|
||||
margin-left: $euiSizeS;
|
||||
}
|
||||
}
|
||||
.filter:last-child {
|
||||
border-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
// SASSTODO: Proper calcs
|
||||
.detector-edit-mode {
|
||||
padding-right: 10px;
|
||||
|
||||
.detector-fields {
|
||||
border-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.help-pane {
|
||||
background-color: $euiFocusBackgroundColor;
|
||||
border: 1px solid darken($euiFocusBackgroundColor, 5%);
|
||||
padding: $euiSize;
|
||||
border-radius: $euiSizeXS;
|
||||
}
|
||||
|
||||
ml-new-job {
|
||||
font-size: $euiFontSizeS;
|
||||
|
||||
.ml_json_tab, .ml_data_preview_tab {
|
||||
|
||||
// SASSTODO: Proper calcs
|
||||
.json-textarea {
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.json-textarea[readonly] {
|
||||
background-color: $euiColorEmptyShade;
|
||||
}
|
||||
|
||||
.note {
|
||||
font-size: $euiFontSizeXS;
|
||||
padding-top: $euiSizeS;
|
||||
font-style: italic;
|
||||
color: $euiColorDarkShade;
|
||||
}
|
||||
}
|
||||
|
||||
i.validation-error {
|
||||
color: $euiColorDanger;
|
||||
text-shadow: 1px 1px 1px $euiColorLightestShade;
|
||||
}
|
||||
|
||||
div.validation-error {
|
||||
color: $euiColorDanger;
|
||||
font-size: $euiFontSizeXS;
|
||||
}
|
||||
|
||||
.tab_contents {
|
||||
padding-top: $euiSize;
|
||||
|
||||
// SASSTODO: Proper calcs
|
||||
.date_container {
|
||||
width: 200px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.lowercase {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
// SASSTODO: Proper calcs
|
||||
.custom-url, .categorization-filter {
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding-right: 30px;
|
||||
button.remove-button {
|
||||
top: 27px;
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
}
|
||||
|
||||
.field-cols {
|
||||
flex: 1 1 1%;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
.categorization-filter {
|
||||
button.remove-button {
|
||||
top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// SASSTODO: Proper calcs
|
||||
.influencer-list-container {
|
||||
@include euiFontSizeXS;
|
||||
|
||||
max-height: 500px;
|
||||
overflow: auto;
|
||||
padding: $euiSizeS $euiSize;
|
||||
color: $euiColorDarkShade;
|
||||
background-color: $euiColorEmptyShade;
|
||||
background-image: none;
|
||||
border: $euiBorderThick;
|
||||
border-radius: $euiBorderRadius;
|
||||
}
|
||||
|
||||
.influencer-list-container {
|
||||
.custom-influencer {
|
||||
margin-top: $euiSize;
|
||||
|
||||
// SASSTODO: Proper calcs and selector
|
||||
input[type='text'] {
|
||||
width:200px;
|
||||
float:left;
|
||||
}
|
||||
|
||||
// SASSTODO: Proper selector
|
||||
button {
|
||||
margin-top: $euiSizeXS;
|
||||
margin-left: $euiSizeXS;
|
||||
}
|
||||
}
|
||||
|
||||
div.checkbox + .custom-influencer {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
small.info {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.time-example {
|
||||
color: $euiColorMediumShade;
|
||||
font-size: $euiFontSizeXS;
|
||||
margin-top: $euiSizeXS;
|
||||
margin-left: $euiSizeXS;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.ml-pre {
|
||||
@include euiFontSizeXS;
|
||||
|
||||
max-height: 500px;
|
||||
overflow: auto;
|
||||
padding: $euiSizeS $euiSize;
|
||||
font-family: $euiCodeFontFamily;
|
||||
color: $euiColorDarkShade;
|
||||
background-color: $euiColorEmptyShade;
|
||||
background-image: none;
|
||||
border: $euiBorderThick;
|
||||
border-radius: $euiBorderRadius;
|
||||
display: block;
|
||||
unicode-bidi: embed;
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
@import 'advanced';
|
||||
@import 'detector_filter_modal/index';
|
||||
@import 'detector_modal/index';
|
||||
@import 'save_status_modal/index';
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import ngMock from 'ng_mock';
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
const mockModalInstance = { close: function () {}, dismiss: function () {} };
|
||||
|
||||
describe('ML - Detector Filter Modal Controller', () => {
|
||||
beforeEach(() => {
|
||||
ngMock.module('kibana');
|
||||
});
|
||||
|
||||
it('Initialize Detector Filter Modal Controller', (done) => {
|
||||
ngMock.inject(function ($rootScope, $controller) {
|
||||
const scope = $rootScope.$new();
|
||||
|
||||
expect(() => {
|
||||
$controller('MlDetectorFilterModal', {
|
||||
$scope: scope,
|
||||
$modalInstance: mockModalInstance,
|
||||
params: { detector: {} }
|
||||
});
|
||||
}).to.not.throwError();
|
||||
|
||||
expect(scope.title).to.eql('Add new filter');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,76 +0,0 @@
|
|||
.detector-filter-modal {
|
||||
padding: $euiSizeL;
|
||||
|
||||
// SASSTODO: Proper selector
|
||||
h3 {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
small {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.filter-field-form {
|
||||
background-color: $euiColorEmptyShade;
|
||||
border: none;
|
||||
|
||||
& > div.field-cols {
|
||||
flex: 1 1 1%;
|
||||
margin-right: $euiSizeXS;
|
||||
}
|
||||
}
|
||||
|
||||
.target-container {
|
||||
border-bottom: $euiBorderThin;
|
||||
}
|
||||
|
||||
.conditions-list-container {
|
||||
|
||||
// SASSTODO: Proper selector
|
||||
.title {
|
||||
font-weight: $euiFontWeightBold;
|
||||
}
|
||||
|
||||
margin-top: $euiSizeXS;
|
||||
padding-top: $euiSizeXS;
|
||||
display: table;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
|
||||
.table-title, .rule-condition {
|
||||
display: table-row;
|
||||
|
||||
// SASSTODO: Proper selector
|
||||
& > div {
|
||||
vertical-align: top;
|
||||
display: table-cell;
|
||||
padding-right: $euiSizeXS;
|
||||
}
|
||||
|
||||
// SASSTODO: Proper selector
|
||||
& > div:last-child {
|
||||
padding-right: 0px;
|
||||
|
||||
button {
|
||||
margin-top: $euiSizeXS;
|
||||
}
|
||||
}
|
||||
|
||||
div.conditions-connective {
|
||||
display: block;
|
||||
text-transform: lowercase;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
// SASSTODO: Proper selector
|
||||
.title {
|
||||
border-bottom: $euiSizeXS solid transparent;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
button.add-new {
|
||||
margin: $euiSizeXS 0px;
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
@import 'detector_filter_modal';
|
|
@ -1,164 +0,0 @@
|
|||
<div class="detector-filter-modal">
|
||||
<ml-message-bar ></ml-message-bar>
|
||||
<h3 class="euiTitle euiTitle--small">{{title}}</h3>
|
||||
<div class="editor-color filter-field-form target-container">
|
||||
|
||||
<datalist id="fields_datalist">
|
||||
<option ng-repeat="field in fields" >{{field}}</option>
|
||||
</datalist>
|
||||
|
||||
<div class="field-cols">
|
||||
<div class="form-group">
|
||||
<ml-form-label label-id="new_filter_targetfieldname">target_field_name</ml-form-label>
|
||||
|
||||
<input
|
||||
aria-labelledby="ml_aria_label_new_filter_targetfieldname"
|
||||
aria-describedby="ml_aria_description_new_filter_targetfieldname"
|
||||
ng-model="filter.target_field_name"
|
||||
tabindex="2"
|
||||
ng-change="functionChange()"
|
||||
list='fields_datalist'
|
||||
class="form-control" />
|
||||
|
||||
<ml-form-label label-id="new_action_targetfieldvalue">target_field_value</ml-form-label>
|
||||
|
||||
<input
|
||||
aria-labelledby="ml_aria_label_new_action_targetfieldvalue"
|
||||
aria-describedby="ml_aria_description_new_action_targetfieldvalue"
|
||||
ng-model="filter.target_field_value"
|
||||
tabindex="2"
|
||||
ng-change="functionChange()"
|
||||
class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-cols">
|
||||
<div class="form-group">
|
||||
<ml-form-label label-id="new_action_conditionsconnective">conditions_connective</ml-form-label>
|
||||
<select
|
||||
aria-labelledby="ml_aria_label_new_action_conditionsconnective"
|
||||
aria-describedby="ml_aria_description_new_action_conditionsconnective"
|
||||
ng-model="filter.conditions_connective"
|
||||
tabindex="2"
|
||||
placeholder=""
|
||||
class="form-control">
|
||||
<option ng-repeat="index in ui.conditions_connective" value="{{index}}" >{{index}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="conditions-list-container" >
|
||||
<div class="title">
|
||||
<span
|
||||
aria-describedby="ml_aria_description_new_action_ruleconditions"
|
||||
i18n-id="xpack.ml.newJob.advanced.detectorFilterModal.conditionsTitle"
|
||||
i18n-default-message="Conditions"
|
||||
></span>
|
||||
<i ml-info-icon="new_action_ruleconditions" />
|
||||
</div>
|
||||
<div class="table-title">
|
||||
<div>
|
||||
<span
|
||||
aria-describedby="ml_aria_description_new_action_conditiontype"
|
||||
i18n-id="xpack.ml.newJob.advanced.detectorFilterModal.conditionTypeTitle"
|
||||
i18n-default-message="Type"
|
||||
></span>
|
||||
<i ml-info-icon="new_action_conditiontype" />
|
||||
</div>
|
||||
<div><span aria-describedby="ml_aria_description_new_action_fieldname">field_name</span><i ml-info-icon="new_action_fieldname" /></div>
|
||||
<div><span aria-describedby="ml_aria_description_new_action_fieldvalue">field_value</span><i ml-info-icon="new_action_fieldvalue" /></div>
|
||||
<div><span aria-describedby="ml_aria_description_new_action_condition">lt/gt</span><i ml-info-icon="new_action_condition" /></div>
|
||||
<div>
|
||||
<span
|
||||
aria-describedby="ml_aria_description_new_action_value"
|
||||
i18n-id="xpack.ml.newJob.advanced.detectorFilterModal.conditionValueLabel"
|
||||
i18n-default-message="value"
|
||||
></span>
|
||||
<i ml-info-icon="new_action_value" />
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div ng-repeat="cond in filter.ruleConditions track by $index" class="rule-condition">
|
||||
<div>
|
||||
<select
|
||||
ng-model="cond.conditionType"
|
||||
placeholder=""
|
||||
class="form-control">
|
||||
<option ng-repeat="type in ui.ruleCondition.conditionType" value="{{type.value}}" >{{type.label}}</option>
|
||||
</select>
|
||||
<div ng-hide="$index === filter.ruleConditions.length-1" class='conditions-connective'>-- {{filter.conditions_connective}} --</div>
|
||||
</div>
|
||||
<div>
|
||||
<select
|
||||
ng-model="cond.field_name"
|
||||
placeholder=""
|
||||
class="form-control">
|
||||
<option ng-repeat="field in fields" >{{field}}</option>
|
||||
</select>
|
||||
<!-- <input
|
||||
ng-model="cond.field_name"
|
||||
tabindex="2"
|
||||
list='fields_datalist'
|
||||
class="form-control" /> -->
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
ng-model="cond.field_value"
|
||||
tabindex="2"
|
||||
class="form-control" />
|
||||
</div>
|
||||
<div>
|
||||
<select
|
||||
ng-model="cond.condition.operator"
|
||||
placeholder=""
|
||||
class="form-control">
|
||||
<option ng-repeat="op in ui.ruleCondition.condition.operator" value="{{op.value}}" >{{op.label}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
ng-model="cond.condition.value"
|
||||
tabindex="2"
|
||||
class="form-control" />
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
aria-label="{{ ::'xpack.ml.newJob.advanced.detectorFilterModal.removeConditionButtonAriaLabel' | i18n: {defaultMessage: 'Remove Condition'} }}"
|
||||
ng-click="removeCondition($index)"
|
||||
tooltip-append-to-body="true"
|
||||
type="button"
|
||||
class="kuiButton kuiButton--danger kuiButton--small remove-button">
|
||||
<i aria-hidden="true" class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
aria-label="{{ ::'xpack.ml.newJob.advanced.detectorFilterModal.addNewConditionButtonAriaLabel' | i18n: {defaultMessage: 'Add new condition'} }}"
|
||||
ng-click="addNewCondition()"
|
||||
type="button"
|
||||
class="kuiButton kuiButton--primary kuiButton--small add-new"
|
||||
i18n-id="xpack.ml.newJob.advanced.detectorFilterModal.addNewConditionButtonLabel"
|
||||
i18n-default-message="{icon} Add new condition"
|
||||
i18n-values="{ html_icon: '<i aria-hidden=\'true\' class=\'fa fa-plus\'></i>' }"
|
||||
></button>
|
||||
|
||||
<hr class="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginMedium">
|
||||
|
||||
<button
|
||||
ng-click="save()"
|
||||
ng-disabled="(saveLock === true || filter.ruleConditions.length === 0)"
|
||||
class="kuiButton kuiButton--primary"
|
||||
aria-label="{{ ::'xpack.ml.newJob.advanced.detectorFilterModal.saveButtonAriaLabel' | i18n: {defaultMessage: 'Save'} }}">
|
||||
{{ (editMode? updateButtonLabel : addButtonLabel) }}
|
||||
</button>
|
||||
<button
|
||||
ng-click="cancel()"
|
||||
ng-disabled="(saveLock === true)"
|
||||
class="kuiButton kuiButton--primary"
|
||||
aria-label="{{ ::'xpack.ml.newJob.advanced.detectorFilterModal.cancelButtonAriaLabel' | i18n: {defaultMessage: 'Cancel'} }}"
|
||||
i18n-id="xpack.ml.newJob.advanced.detectorFilterModal.cancelButtonLabel"
|
||||
i18n-default-message="Cancel"
|
||||
></button>
|
||||
</div>
|
|
@ -1,251 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import _ from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import angular from 'angular';
|
||||
|
||||
import { mlMessageBarService } from 'plugins/ml/components/messagebar/messagebar_service';
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml');
|
||||
|
||||
module.controller('MlDetectorFilterModal', function ($scope, $modalInstance, params) {
|
||||
const msgs = mlMessageBarService;
|
||||
msgs.clear();
|
||||
$scope.title = i18n.translate('xpack.ml.newJob.advanced.detectorFilterModal.addNewFilterTitle', {
|
||||
defaultMessage: 'Add new filter'
|
||||
});
|
||||
$scope.detector = params.detector;
|
||||
$scope.saveLock = false;
|
||||
$scope.editMode = false;
|
||||
let index = -1;
|
||||
const add = params.add;
|
||||
const validate = params.validate;
|
||||
|
||||
$scope.updateButtonLabel = i18n.translate('xpack.ml.newJob.advanced.detectorFilterModal.updateButtonLabel', {
|
||||
defaultMessage: 'Update'
|
||||
});
|
||||
$scope.addButtonLabel = i18n.translate('xpack.ml.newJob.advanced.detectorFilterModal.addButtonLabel', {
|
||||
defaultMessage: 'Add'
|
||||
});
|
||||
|
||||
/*
|
||||
$scope.functions = [
|
||||
{id: 'count', uri: 'count.html#count'},
|
||||
{id: 'low_count', uri: 'count.html#count'},
|
||||
{id: 'high_count', uri: 'count.html#count'},
|
||||
{id: 'non_zero_count', uri: 'count.html#non-zero-count'},
|
||||
{id: 'low_non_zero_count', uri: 'count.html#non-zero-count'},
|
||||
{id: 'high_non_zero_count', uri: 'count.html#non-zero-count'},
|
||||
{id: 'distinct_count', uri: 'count.html#distinct-count'},
|
||||
{id: 'low_distinct_count', uri: 'count.html#distinct-count'},
|
||||
{id: 'high_distinct_count', uri: 'count.html#distinct-count'},
|
||||
{id: 'rare', uri: 'rare.html#rare'},
|
||||
{id: 'freq_rare', uri: 'rare.html#freq-rare'},
|
||||
{id: 'info_content', uri: 'info_content.html#info-content'},
|
||||
{id: 'low_info_content', uri: 'info_content.html#info-content'},
|
||||
{id: 'high_info_content', uri: 'info_content.html#info-content'},
|
||||
{id: 'metric', uri: 'metric.html#metric'},
|
||||
{id: 'mean', uri: 'metric.html#mean'},
|
||||
{id: 'low_mean', uri: 'metric.html#mean'},
|
||||
{id: 'high_mean', uri: 'metric.html#mean'},
|
||||
{id: 'min', uri: 'metric.html#min'},
|
||||
{id: 'max', uri: 'metric.html#max'},
|
||||
{id: 'varp', uri: 'metric.html#varp'},
|
||||
{id: 'low_varp', uri: 'metric.html#varp'},
|
||||
{id: 'high_varp', uri: 'metric.html#varp'},
|
||||
{id: 'sum', uri: 'sum.html#sum'},
|
||||
{id: 'low_sum', uri: 'sum.html#sum'},
|
||||
{id: 'high_sum', uri: 'sum.html#sum'},
|
||||
{id: 'non_null_sum', uri: 'sum.html#non-null-sum'},
|
||||
{id: 'low_non_null_sum', uri: 'sum.html#non-null-sum'},
|
||||
{id: 'high_non_null_sum', uri: 'sum.html#non-null-sum'},
|
||||
{id: 'time_of_day', uri: 'time.html#time-of-day'},
|
||||
{id: 'time_of_week', uri: 'time.html#time-of-week'},
|
||||
{id: 'lat_long', uri: 'geographic.html'},
|
||||
];
|
||||
*/
|
||||
$scope.fields = [];
|
||||
if ($scope.detector.field_name) {
|
||||
$scope.fields.push($scope.detector.field_name);
|
||||
}
|
||||
if ($scope.detector.by_field_name) {
|
||||
$scope.fields.push($scope.detector.by_field_name);
|
||||
}
|
||||
if ($scope.detector.over_field_name) {
|
||||
$scope.fields.push($scope.detector.over_field_name);
|
||||
}
|
||||
if ($scope.detector.partition_field_name) {
|
||||
$scope.fields.push($scope.detector.partition_field_name);
|
||||
}
|
||||
|
||||
|
||||
// creating a new filter
|
||||
if (params.filter === undefined) {
|
||||
$scope.filter = {
|
||||
ruleAction: 'filter_results',
|
||||
target_field_name: '',
|
||||
target_field_value: '',
|
||||
conditions_connective: 'or',
|
||||
conditions: [],
|
||||
value_list: []
|
||||
};
|
||||
} else {
|
||||
// editing an existing filter
|
||||
$scope.editMode = true;
|
||||
$scope.filter = params.filter;
|
||||
$scope.title = i18n.translate('xpack.ml.newJob.advanced.detectorFilterModal.editFilterTitle', {
|
||||
defaultMessage: 'Edit filter'
|
||||
});
|
||||
index = params.index;
|
||||
}
|
||||
|
||||
$scope.ui = {
|
||||
ruleAction: ['filter_results'],
|
||||
target_field_name: '',
|
||||
target_field_value: '',
|
||||
conditions_connective: ['or', 'and'],
|
||||
ruleCondition: {
|
||||
condition_type: [{
|
||||
label: 'actual',
|
||||
value: 'numerical_actual'
|
||||
}, {
|
||||
label: 'typical',
|
||||
value: 'numerical_typical'
|
||||
}, {
|
||||
label: '|actual - typical|',
|
||||
value: 'numerical_diff_abs'
|
||||
}/*, {
|
||||
label: 'Categorical',
|
||||
value: 'categorical'
|
||||
}*/
|
||||
],
|
||||
field_name: '',
|
||||
field_value: '',
|
||||
condition: {
|
||||
operator: [{
|
||||
label: '<',
|
||||
value: 'lt'
|
||||
}, {
|
||||
label: '>',
|
||||
value: 'gt'
|
||||
}, {
|
||||
label: '<=',
|
||||
value: 'lte'
|
||||
}, {
|
||||
label: '>=',
|
||||
value: 'gte'
|
||||
}]
|
||||
},
|
||||
value_list: []
|
||||
}
|
||||
};
|
||||
|
||||
$scope.addNewCondition = function () {
|
||||
$scope.filter.conditions.push({
|
||||
condition_type: 'numerical_actual',
|
||||
field_name: '',
|
||||
field_value: '',
|
||||
condition: {
|
||||
operator: 'lt',
|
||||
value: ''
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeCondition = function (idx) {
|
||||
$scope.filter.conditions.splice(idx, 1);
|
||||
};
|
||||
|
||||
|
||||
// console.log('MlDetectorFilterModal detector:', $scope.detector)
|
||||
|
||||
$scope.helpLink = {};
|
||||
|
||||
// $scope.functionChange = function() {
|
||||
// const func = _.findWhere($scope.functions, {id: $scope.detector.function});
|
||||
// $scope.helpLink.uri = 'functions/';
|
||||
// $scope.helpLink.label = 'Help for ';
|
||||
|
||||
// if (func) {
|
||||
// $scope.helpLink.uri += func.uri;
|
||||
// $scope.helpLink.label += func.id;
|
||||
// } else {
|
||||
// $scope.helpLink.uri += 'functions.html';
|
||||
// $scope.helpLink.label += 'analytical functions';
|
||||
// }
|
||||
// };
|
||||
|
||||
// $scope.functionChange();
|
||||
|
||||
$scope.save = function () {
|
||||
const filter = angular.copy($scope.filter);
|
||||
|
||||
if (!filter.conditions.length) {
|
||||
return;
|
||||
}
|
||||
$scope.saveLock = true;
|
||||
|
||||
// remove any properties that aren't being used
|
||||
if (filter.target_field_name === '') {
|
||||
delete filter.target_field_name;
|
||||
}
|
||||
if (filter.target_field_value === '') {
|
||||
delete filter.target_field_value;
|
||||
}
|
||||
|
||||
_.each(filter.conditions, (cond) => {
|
||||
delete cond.$$hashKey;
|
||||
if (cond.field_name === '') {
|
||||
delete cond.field_vname;
|
||||
}
|
||||
if (cond.fieldValue === '') {
|
||||
delete cond.fieldValue;
|
||||
}
|
||||
});
|
||||
|
||||
if (filter.value_list && filter.value_list.length === 0) {
|
||||
delete filter.value_list;
|
||||
}
|
||||
|
||||
// make a local copy of the detector, add the new filter
|
||||
// and send it off for validation.
|
||||
// if it passes, add the filter to the real detector.
|
||||
const dtr = angular.copy($scope.detector);
|
||||
if (dtr.rules === undefined) {
|
||||
dtr.rules = [];
|
||||
}
|
||||
|
||||
if (index >= 0) {
|
||||
dtr.rules[index] = filter;
|
||||
} else {
|
||||
dtr.rules.push(filter);
|
||||
}
|
||||
|
||||
validate(dtr)
|
||||
.then((resp) => {
|
||||
msgs.clear();
|
||||
$scope.saveLock = false;
|
||||
if (resp.success) {
|
||||
add($scope.detector, filter, index);
|
||||
|
||||
// console.log('save:', filter);
|
||||
$modalInstance.close();
|
||||
|
||||
} else {
|
||||
msgs.error(resp.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.cancel = function () {
|
||||
msgs.clear();
|
||||
$modalInstance.close();
|
||||
};
|
||||
});
|
|
@ -1,9 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import './detector_filter_modal_controller';
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import ngMock from 'ng_mock';
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
const mockModalInstance = { close: function () {}, dismiss: function () {} };
|
||||
|
||||
describe('ML - Detector Modal Controller', () => {
|
||||
beforeEach(() => {
|
||||
ngMock.module('kibana');
|
||||
});
|
||||
|
||||
it('Initialize Detector Modal Controller', (done) => {
|
||||
ngMock.inject(function ($rootScope, $controller) {
|
||||
const scope = $rootScope.$new();
|
||||
|
||||
expect(() => {
|
||||
$controller('MlDetectorModal', {
|
||||
$scope: scope,
|
||||
$modalInstance: mockModalInstance,
|
||||
params: {}
|
||||
});
|
||||
}).to.not.throwError();
|
||||
|
||||
expect(scope.title).to.eql('Add new detector');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,28 +0,0 @@
|
|||
.detector-modal {
|
||||
font-size: $euiFontSizeS;
|
||||
padding: $euiSizeL;
|
||||
|
||||
// SASSTODO: Proper selector
|
||||
h3 {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
small {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.detector_field_form {
|
||||
background-color: $euiColorEmptyShade;
|
||||
border: none;
|
||||
display: flex;
|
||||
|
||||
& > div.field-cols {
|
||||
flex: 1 1 1%;
|
||||
margin-right: $euiSizeXS;
|
||||
|
||||
select {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
@import 'detector_modal';
|
|
@ -1,121 +0,0 @@
|
|||
<div class="detector-modal">
|
||||
<ml-message-bar ></ml-message-bar>
|
||||
<h1 class="euiTitle">{{title}}</h1>
|
||||
<div class="euiSpacer euiSpacer--m"></div>
|
||||
|
||||
<div class="form-group">
|
||||
<ml-form-label label-id="new_job_detector_description">
|
||||
{{ ::'xpack.ml.newJob.advanced.detectorModal.descriptionLabel' | i18n: {defaultMessage: 'Description'} }}
|
||||
</ml-form-label>
|
||||
<input
|
||||
aria-labelledby="ml_aria_label_new_job_detector_description"
|
||||
aria-describedby="ml_aria_description_new_job_detector_description"
|
||||
ng-model="detector.detector_description"
|
||||
placeholder="{{ detectorToString(detector) }}"
|
||||
class="form-control" />
|
||||
</div>
|
||||
|
||||
<div class="editor-color detector_field_form">
|
||||
|
||||
<div class="field-cols">
|
||||
<div class="form-group">
|
||||
<ml-form-label label-id="new_job_detector_function">function</ml-form-label>
|
||||
<field-select
|
||||
label-id='"new_job_detector_function"'
|
||||
on-change='setDetectorProperty'
|
||||
value='detector.function'
|
||||
field='"function"'
|
||||
options='functionIds'>
|
||||
</field-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-cols">
|
||||
<div class="form-group">
|
||||
<ml-form-label label-id="new_job_detector_fieldname">field_name</ml-form-label>
|
||||
<field-select
|
||||
label-id='"new_job_detector_fieldname"'
|
||||
on-change='setDetectorProperty'
|
||||
value='detector.field_name'
|
||||
field='"field_name"'
|
||||
options='fields'>
|
||||
</field-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-cols">
|
||||
<div class="form-group">
|
||||
<ml-form-label label-id="new_job_detector_byfieldname">by_field_name</ml-form-label>
|
||||
<field-select
|
||||
label-id='"new_job_detector_byfieldname"'
|
||||
on-change='setDetectorProperty'
|
||||
value='detector.by_field_name'
|
||||
field='"by_field_name"'
|
||||
options='fields_byFieldName'>
|
||||
</field-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="editor-color detector_field_form">
|
||||
|
||||
<div class="field-cols">
|
||||
<div class="form-group">
|
||||
<ml-form-label label-id="new_job_detector_overfieldname">over_field_name</ml-form-label>
|
||||
<field-select
|
||||
label-id='"new_job_detector_overfieldname"'
|
||||
on-change='setDetectorProperty'
|
||||
value='detector.over_field_name'
|
||||
field='"over_field_name"'
|
||||
options='fields'>
|
||||
</field-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-cols">
|
||||
<div class="form-group">
|
||||
<ml-form-label label-id="new_job_detector_partitionfieldname">partition_field_name</ml-form-label>
|
||||
<field-select
|
||||
label-id='"new_job_detector_partitionfieldname"'
|
||||
on-change='setDetectorProperty'
|
||||
value='detector.partition_field_name'
|
||||
field='"partition_field_name"'
|
||||
options='fields'>
|
||||
</field-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-cols">
|
||||
<div class="form-group">
|
||||
<ml-form-label label-id="new_job_detector_excludefrequent">exclude_frequent</ml-form-label>
|
||||
<field-select
|
||||
label-id='"new_job_detector_excludefrequent"'
|
||||
on-change='setDetectorProperty'
|
||||
value='detector.exclude_frequent'
|
||||
field='"exclude_frequent"'
|
||||
options='{all: "", none: ""}'>
|
||||
</field-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<small><div class='help-pane'><ml-documentation-help-link ml-uri="{{helpLink.uri}}" ml-label="{{helpLink.label}}"></ml-documentation-help-link></div></small>
|
||||
|
||||
<hr class="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginMedium">
|
||||
|
||||
<button
|
||||
ng-click="save()"
|
||||
ng-disabled="(saveLock === true) || (detector.function === '')"
|
||||
class="kuiButton kuiButton--primary"
|
||||
aria-label="{{ ::'xpack.ml.newJob.advanced.detectorModal.saveButtonAriaLabel' | i18n: {defaultMessage: 'Save'} }}">
|
||||
{{ (editMode ? updateButtonLabel : addButtonLabel) }}
|
||||
</button>
|
||||
<button
|
||||
ng-click="cancel()"
|
||||
ng-disabled="(saveLock === true)"
|
||||
class="kuiButton kuiButton--primary"
|
||||
aria-label="{{ ::'xpack.ml.newJob.advanced.detectorModal.cancelButtonAriaLabel' | i18n: {defaultMessage: 'Cancel'} }}"
|
||||
i18n-id="xpack.ml.newJob.advanced.detectorModal.cancelButtonLabel"
|
||||
i18n-default-message="Cancel"
|
||||
></button>
|
||||
</div>
|
|
@ -1,159 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import _ from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import angular from 'angular';
|
||||
import { detectorToString } from 'plugins/ml/util/string_utils';
|
||||
import { mlMessageBarService } from 'plugins/ml/components/messagebar/messagebar_service';
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml');
|
||||
|
||||
module.controller('MlDetectorModal', function ($scope, $modalInstance, params) {
|
||||
const msgs = mlMessageBarService;
|
||||
msgs.clear();
|
||||
$scope.title = i18n.translate('xpack.ml.newJob.advanced.detectorModal.addNewDetectorTitle', {
|
||||
defaultMessage: 'Add new detector'
|
||||
});
|
||||
$scope.detector = { 'function': '' };
|
||||
$scope.saveLock = false;
|
||||
$scope.editMode = false;
|
||||
let index = -1;
|
||||
|
||||
$scope.updateButtonLabel = i18n.translate('xpack.ml.newJob.advanced.detectorModal.updateButtonLabel', {
|
||||
defaultMessage: 'Update'
|
||||
});
|
||||
$scope.addButtonLabel = i18n.translate('xpack.ml.newJob.advanced.detectorModal.addButtonLabel', {
|
||||
defaultMessage: 'Add'
|
||||
});
|
||||
|
||||
$scope.functions = [
|
||||
{ id: 'count', uri: 'ml-count-functions.html#ml-count' },
|
||||
{ id: 'low_count', uri: 'ml-count-functions.html#ml-count' },
|
||||
{ id: 'high_count', uri: 'ml-count-functions.html#ml-count' },
|
||||
{ id: 'non_zero_count', uri: 'ml-count-functions.html#ml-nonzero-count' },
|
||||
{ id: 'low_non_zero_count', uri: 'ml-count-functions.html#ml-nonzero-count' },
|
||||
{ id: 'high_non_zero_count', uri: 'ml-count-functions.html#ml-nonzero-count' },
|
||||
{ id: 'distinct_count', uri: 'ml-count-functions.html#ml-distinct-count' },
|
||||
{ id: 'low_distinct_count', uri: 'ml-count-functions.html#ml-distinct-count' },
|
||||
{ id: 'high_distinct_count', uri: 'ml-count-functions.html#ml-distinct-count' },
|
||||
{ id: 'rare', uri: 'ml-rare-functions.html#ml-rare' },
|
||||
{ id: 'freq_rare', uri: 'ml-rare-functions.html#ml-freq-rare' },
|
||||
{ id: 'info_content', uri: 'ml-info-functions.html#ml-info-content' },
|
||||
{ id: 'low_info_content', uri: 'ml-info-functions.html#ml-info-content' },
|
||||
{ id: 'high_info_content', uri: 'ml-info-functions.html#ml-info-content' },
|
||||
{ id: 'metric', uri: 'ml-metric-functions.html#ml-metric-metric' },
|
||||
{ id: 'median', uri: 'ml-metric-functions.html#ml-metric-median' },
|
||||
{ id: 'low_median', uri: 'ml-metric-functions.html#ml-metric-median' },
|
||||
{ id: 'high_median', uri: 'ml-metric-functions.html#ml-metric-median' },
|
||||
{ id: 'mean', uri: 'ml-metric-functions.html#ml-metric-mean' },
|
||||
{ id: 'low_mean', uri: 'ml-metric-functions.html#ml-metric-mean' },
|
||||
{ id: 'high_mean', uri: 'ml-metric-functions.html#ml-metric-mean' },
|
||||
{ id: 'min', uri: 'ml-metric-functions.html#ml-metric-min' },
|
||||
{ id: 'max', uri: 'ml-metric-functions.html#ml-metric-max' },
|
||||
{ id: 'varp', uri: 'ml-metric-functions.html#ml-metric-varp' },
|
||||
{ id: 'low_varp', uri: 'ml-metric-functions.html#ml-metric-varp' },
|
||||
{ id: 'high_varp', uri: 'ml-metric-functions.html#ml-metric-varp' },
|
||||
{ id: 'sum', uri: 'ml-sum-functions.html#ml-sum' },
|
||||
{ id: 'low_sum', uri: 'ml-sum-functions.html#ml-sum' },
|
||||
{ id: 'high_sum', uri: 'ml-sum-functions.html#ml-sum' },
|
||||
{ id: 'non_null_sum', uri: 'ml-sum-functions.html#ml-nonnull-sum' },
|
||||
{ id: 'low_non_null_sum', uri: 'ml-sum-functions.html#ml-nonnull-sum' },
|
||||
{ id: 'high_non_null_sum', uri: 'ml-sum-functions.html#ml-nonnull-sum' },
|
||||
{ id: 'time_of_day', uri: 'ml-time-functions.html#ml-time-of-day' },
|
||||
{ id: 'time_of_week', uri: 'ml-time-functions.html#ml-time-of-week' },
|
||||
{ id: 'lat_long', uri: 'ml-geo-functions.html#ml-lat-long' },
|
||||
];
|
||||
|
||||
$scope.functionIds = {};
|
||||
_.each($scope.functions, (f) => {
|
||||
$scope.functionIds[f.id] = '';
|
||||
});
|
||||
|
||||
$scope.fields = params.fields;
|
||||
|
||||
// fields list for by_field_name field only
|
||||
$scope.fields_byFieldName = angular.copy($scope.fields);
|
||||
// if data has been added to the categorizationFieldName,
|
||||
// add the option mlcategory to the by_field_name datalist
|
||||
if (params.catFieldNameSelected) {
|
||||
$scope.fields_byFieldName.mlcategory = 'mlcategory';
|
||||
}
|
||||
|
||||
const validate = params.validate;
|
||||
const add = params.add;
|
||||
if (params.detector) {
|
||||
$scope.detector = params.detector;
|
||||
index = params.index;
|
||||
$scope.title = i18n.translate('xpack.ml.newJob.advanced.detectorModal.editDetectorTitle', {
|
||||
defaultMessage: 'Edit detector'
|
||||
});
|
||||
$scope.editMode = true;
|
||||
}
|
||||
|
||||
$scope.detectorToString = detectorToString;
|
||||
|
||||
$scope.helpLink = {};
|
||||
|
||||
$scope.functionChange = function () {
|
||||
const func = _.findWhere($scope.functions, { id: $scope.detector.function });
|
||||
$scope.helpLink.label = i18n.translate('xpack.ml.newJob.advanced.detectorModal.helpForAnalyticalFunctionsLabel', {
|
||||
defaultMessage: 'Help for analytical functions'
|
||||
});
|
||||
$scope.helpLink.uri = 'ml-functions.html';
|
||||
|
||||
if (func) {
|
||||
$scope.helpLink.uri = func.uri;
|
||||
$scope.helpLink.label = i18n.translate('xpack.ml.newJob.advanced.detectorModal.helpForAnalyticalFunctionLabel', {
|
||||
defaultMessage: 'Help for {funcId}',
|
||||
values: { funcId: func.id }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.functionChange();
|
||||
|
||||
$scope.setDetectorProperty = function (value, field) {
|
||||
if (value === '' || value === undefined) {
|
||||
// remove the property from the detector JSON
|
||||
delete $scope.detector[field];
|
||||
} else {
|
||||
$scope.detector[field] = value;
|
||||
}
|
||||
|
||||
if (field === 'function') {
|
||||
$scope.functionChange();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.save = function () {
|
||||
$scope.saveLock = true;
|
||||
validate($scope.detector)
|
||||
.then((resp) => {
|
||||
$scope.saveLock = false;
|
||||
if (resp.success) {
|
||||
if ($scope.detector.detector_description === '') {
|
||||
// remove blank description so server generated one is used.
|
||||
delete $scope.detector.detector_description;
|
||||
}
|
||||
add($scope.detector, index);
|
||||
$modalInstance.close($scope.detector);
|
||||
msgs.clear();
|
||||
|
||||
} else {
|
||||
msgs.error(resp.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.cancel = function () {
|
||||
msgs.clear();
|
||||
$modalInstance.close();
|
||||
};
|
||||
});
|
|
@ -1,10 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import './detector_modal_controller';
|
||||
import 'plugins/ml/components/documentation_help_link';
|
|
@ -1,78 +0,0 @@
|
|||
<div>
|
||||
<div class="form-group">
|
||||
<div ng-repeat="detector in detectors track by $index">
|
||||
<div class="detector" ng-class="{'detector-edit-mode':(editMode==='EDIT')}">
|
||||
<div class="detector-fields">
|
||||
<label
|
||||
ng-show="editMode==='EDIT'"
|
||||
i18n-id="xpack.ml.newJob.advanced.detectorsList.detectorLabel"
|
||||
i18n-default-message="Detector:"
|
||||
></label>
|
||||
<div
|
||||
aria-label="{{ ::'xpack.ml.newJob.advanced.detectorsList.customisedDescriptionAriaLabel' | i18n: {defaultMessage: 'Customised description'} }}"
|
||||
ng-hide="editMode==='EDIT' || detector.detector_description === '' || detector.detector_description === detectorToString(detector) "
|
||||
>
|
||||
{{ detector.detector_description }}
|
||||
</div>
|
||||
<div
|
||||
aria-label="{{ ::'xpack.ml.newJob.advanced.detectorsList.defaultDescriptionAriaLabel' | i18n: {defaultMessage: 'Default description'} }}"
|
||||
style="font-style:italic;"
|
||||
>
|
||||
{{ detectorToString(detector) }}
|
||||
</div>
|
||||
|
||||
<div ng-show="editMode==='EDIT'">
|
||||
<label
|
||||
class="kuiFormLabel"
|
||||
i18n-id="xpack.ml.newJob.advanced.detectorsList.descriptionLabel"
|
||||
i18n-default-message="Description:"
|
||||
></label>
|
||||
<input
|
||||
ng-model="detector.detector_description"
|
||||
class="form-control" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="button-container" ng-show="editMode==='NEW'">
|
||||
<button
|
||||
ng-click="editDetector($index)"
|
||||
aria-label="{{ ::'xpack.ml.newJob.advanced.detectorsList.editButtonAriaLabel' | i18n: {defaultMessage: 'Edit'} }}"
|
||||
class="kuiButton kuiButton--basic kuiButton--small"
|
||||
data-toggle="tooltip"
|
||||
tooltip="{{ ::'xpack.ml.newJob.advanced.detectorsList.editButtonTooltip' | i18n: {defaultMessage: 'Edit Detector'} }}">
|
||||
<i aria-hidden="true" class="fa fa-pencil"></i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
aria-label="{{ ::'xpack.ml.newJob.advanced.detectorsList.removeDetectorButtonAriaLabel' | i18n: {defaultMessage: 'Remove Detector'} }}"
|
||||
ng-click="removeDetector($index)"
|
||||
tooltip-append-to-body="true"
|
||||
type="button"
|
||||
class="kuiButton kuiButton--danger kuiButton--small remove-button">
|
||||
<i aria-hidden="true" class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div ng-show="editMode==='NEW'">
|
||||
<button
|
||||
aria-labelledby="ml_aria_description_new_job_detectors"
|
||||
aria-describedby="ml_aria_label_new_job_add_detector"
|
||||
ng-click="openNewWindow()"
|
||||
type="button"
|
||||
class="kuiButton kuiButton--primary kuiButton--small">
|
||||
<i aria-hidden="true" class="fa fa-plus"></i>
|
||||
<span
|
||||
id="ml_aria_label_new_job_add_detector"
|
||||
i18n-id="xpack.ml.newJob.advanced.detectorsList.addDetectorButtonLabel"
|
||||
i18n-default-message="Add Detector"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -1,191 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
// directive for displaying detectors form list.
|
||||
|
||||
import angular from 'angular';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import _ from 'lodash';
|
||||
import 'plugins/ml/jobs/new_job/advanced/detector_modal';
|
||||
import 'plugins/ml/jobs/new_job/advanced/detector_filter_modal';
|
||||
import { detectorToString } from 'plugins/ml/util/string_utils';
|
||||
import template from './detectors_list.html';
|
||||
import detectorModalTemplate from 'plugins/ml/jobs/new_job/advanced/detector_modal/detector_modal.html';
|
||||
import detectorFilterModalTemplate from 'plugins/ml/jobs/new_job/advanced/detector_filter_modal/detector_filter_modal.html';
|
||||
import { mlJobService } from 'plugins/ml/services/job_service';
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml');
|
||||
|
||||
module.directive('mlJobDetectorsList', function ($modal) {
|
||||
return {
|
||||
restrict: 'AE',
|
||||
replace: true,
|
||||
scope: {
|
||||
detectors: '=mlDetectors',
|
||||
indices: '=mlIndices',
|
||||
fields: '=mlFields',
|
||||
catFieldNameSelected: '=mlCatFieldNameSelected',
|
||||
editMode: '=mlEditMode',
|
||||
onUpdate: '=mlOnDetectorsUpdate'
|
||||
},
|
||||
template,
|
||||
controller: function ($scope) {
|
||||
|
||||
$scope.addDetector = function (dtr, index) {
|
||||
if (dtr !== undefined) {
|
||||
if (index >= 0) {
|
||||
$scope.detectors[index] = dtr;
|
||||
} else {
|
||||
$scope.detectors.push(dtr);
|
||||
}
|
||||
|
||||
$scope.onUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.removeDetector = function (index) {
|
||||
$scope.detectors.splice(index, 1);
|
||||
$scope.onUpdate();
|
||||
};
|
||||
|
||||
$scope.editDetector = function (index) {
|
||||
$scope.openNewWindow(index);
|
||||
};
|
||||
|
||||
$scope.info = function () {
|
||||
|
||||
};
|
||||
|
||||
// add a filter to the detector
|
||||
// called from inside the filter modal
|
||||
$scope.addFilter = function (dtr, filter, filterIndex) {
|
||||
if (dtr.rules === undefined) {
|
||||
dtr.rules = [];
|
||||
}
|
||||
|
||||
if (filterIndex >= 0) {
|
||||
dtr.rules[filterIndex] = filter;
|
||||
} else {
|
||||
dtr.rules.push(filter);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.removeFilter = function (detector, filterIndex) {
|
||||
detector.rules.splice(filterIndex, 1);
|
||||
};
|
||||
|
||||
$scope.editFilter = function (detector, index) {
|
||||
$scope.openFilterWindow(detector, index);
|
||||
};
|
||||
|
||||
|
||||
$scope.detectorToString = detectorToString;
|
||||
|
||||
function validateDetector(dtr) {
|
||||
|
||||
// locally check exclude_frequent as it can only be 'true', 'false', 'by' or 'over'
|
||||
if (dtr.exclude_frequent !== undefined && dtr.exclude_frequent !== '') {
|
||||
const exFrqs = ['all', 'none', 'by', 'over'];
|
||||
if (_.indexOf(exFrqs, dtr.exclude_frequent.trim()) === -1) {
|
||||
// return a pretend promise
|
||||
return {
|
||||
then: function (callback) {
|
||||
callback({
|
||||
success: false,
|
||||
message: i18n.translate('xpack.ml.newJob.advanced.detectorsList.invalidExcludeFrequentParameterErrorMessage', {
|
||||
defaultMessage: '{excludeFrequentParam} value must be: {allValue}, {noneValue}, {byValue} or {overValue}',
|
||||
values: {
|
||||
excludeFrequentParam: 'exclude_frequent',
|
||||
allValue: '"all"',
|
||||
noneValue: '"none"',
|
||||
byValue: '"by"',
|
||||
overValue: '"over"'
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// post detector to server for in depth validation
|
||||
return mlJobService.validateDetector(dtr)
|
||||
.then((resp) => {
|
||||
return {
|
||||
success: (resp.acknowledged || false)
|
||||
};
|
||||
})
|
||||
.catch((resp) => {
|
||||
return {
|
||||
success: false,
|
||||
message: (
|
||||
resp.message || i18n.translate('xpack.ml.newJob.advanced.detectorsList.validationFailedErrorMessage', {
|
||||
defaultMessage: 'Validation failed'
|
||||
})
|
||||
)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
$scope.openNewWindow = function (index) {
|
||||
index = (index !== undefined ? index : -1);
|
||||
let dtr;
|
||||
if (index >= 0) {
|
||||
dtr = angular.copy($scope.detectors[index]);
|
||||
}
|
||||
$modal.open({
|
||||
template: detectorModalTemplate,
|
||||
controller: 'MlDetectorModal',
|
||||
backdrop: 'static',
|
||||
keyboard: false,
|
||||
size: 'lg',
|
||||
resolve: {
|
||||
params: function () {
|
||||
return {
|
||||
fields: $scope.fields,
|
||||
validate: validateDetector,
|
||||
detector: dtr,
|
||||
index: index,
|
||||
add: $scope.addDetector,
|
||||
catFieldNameSelected: $scope.catFieldNameSelected
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.openFilterWindow = function (dtr, filterIndex) {
|
||||
filterIndex = (filterIndex !== undefined ? filterIndex : -1);
|
||||
let filter;
|
||||
if (filterIndex >= 0) {
|
||||
filter = angular.copy(dtr.rules[filterIndex]);
|
||||
}
|
||||
$modal.open({
|
||||
template: detectorFilterModalTemplate,
|
||||
controller: 'MlDetectorFilterModal',
|
||||
backdrop: 'static',
|
||||
keyboard: false,
|
||||
size: 'lg',
|
||||
resolve: {
|
||||
params: function () {
|
||||
return {
|
||||
fields: $scope.fields,
|
||||
validate: validateDetector,
|
||||
detector: dtr,
|
||||
filter: filter,
|
||||
index: filterIndex,
|
||||
add: $scope.addFilter
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { EnableModelPlotCallout } from './enable_model_plot_callout_view.js';
|
||||
|
||||
const message = 'Test message';
|
||||
|
||||
describe('EnableModelPlotCallout', () => {
|
||||
|
||||
test('Callout is rendered correctly with message', () => {
|
||||
const wrapper = mountWithIntl(<EnableModelPlotCallout message={message} />);
|
||||
const calloutText = wrapper.find('EuiText');
|
||||
|
||||
expect(calloutText.text()).toBe(message);
|
||||
});
|
||||
|
||||
});
|
|
@ -1,25 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import 'ngreact';
|
||||
|
||||
import { wrapInI18nContext } from 'ui/i18n';
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml', ['react']);
|
||||
|
||||
import { EnableModelPlotCallout } from './enable_model_plot_callout_view.js';
|
||||
|
||||
module.directive('mlEnableModelPlotCallout', function (reactDirective) {
|
||||
return reactDirective(
|
||||
wrapInI18nContext(
|
||||
EnableModelPlotCallout,
|
||||
undefined,
|
||||
{ restrict: 'E' }
|
||||
)
|
||||
);
|
||||
});
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
import {
|
||||
EuiCallOut,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
|
||||
export const EnableModelPlotCallout = ({ message }) => (
|
||||
<Fragment>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiCallOut
|
||||
title={<FormattedMessage
|
||||
id="xpack.ml.newJob.advanced.enableModelPlot.proceedWithCautionTitle"
|
||||
defaultMessage="Proceed with caution!"
|
||||
/>}
|
||||
color="warning"
|
||||
iconType="help"
|
||||
>
|
||||
<p>
|
||||
{message}
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
EnableModelPlotCallout.propTypes = {
|
||||
message: PropTypes.string.isRequired,
|
||||
};
|
|
@ -1,8 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
import './enable_model_plot_callout_directive.js';
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import _ from 'lodash';
|
||||
import Select from 'react-select';
|
||||
import 'react-select/dist/react-select.css';
|
||||
|
||||
export class FieldSelect extends Component {
|
||||
render() {
|
||||
const {
|
||||
labelId,
|
||||
onChange,
|
||||
value,
|
||||
options,
|
||||
field,
|
||||
placeholder
|
||||
} = this.props;
|
||||
|
||||
function change(selection) {
|
||||
const val = (selection ? selection.value : '');
|
||||
onChange(val, field);
|
||||
}
|
||||
|
||||
function getOptions() {
|
||||
const ops = [];
|
||||
_.each(options, (op, key) => {
|
||||
ops.push({ label: key, value: key });
|
||||
});
|
||||
return ops;
|
||||
}
|
||||
|
||||
return (
|
||||
<Select
|
||||
aria-describedby={'ml_aria_description_' + labelId}
|
||||
aria-labelledby={'ml_aria_label_' + labelId}
|
||||
placeholder={placeholder}
|
||||
options={getOptions()}
|
||||
value={value}
|
||||
onChange={change}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FieldSelect.propTypes = {
|
||||
labelId: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
options: PropTypes.object,
|
||||
field: PropTypes.string,
|
||||
placeholder: PropTypes.string
|
||||
};
|
|
@ -1,17 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
import 'ngreact';
|
||||
import { FieldSelect } from './field_select';
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml', ['react']);
|
||||
module.directive('fieldSelect', function (reactDirective) {
|
||||
return reactDirective(FieldSelect);
|
||||
});
|
|
@ -1,14 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import './new_job_controller';
|
||||
import './detectors_list_directive';
|
||||
import './save_status_modal';
|
||||
import './field_select_directive';
|
||||
import 'plugins/ml/components/job_group_select';
|
||||
import './enable_model_plot_callout';
|
|
@ -1,686 +0,0 @@
|
|||
<ml-nav-menu name="new_job_advanced" />
|
||||
<ml-new-job class="ml-new-job euiPage euiPage--widthIsNotRestricted">
|
||||
<ml-message-bar></ml-message-bar>
|
||||
<div ng-controller="MlNewJob" class="euiPageBody">
|
||||
<div class="euiPanel euiPanel--paddingLarge euiPageContent">
|
||||
<div class="euiPageContentHeader">
|
||||
<div class="euiPageContentHeaderSection">
|
||||
<h3 class="euiTitle euiTitle--large">{{ui.pageTitle}}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="euiPageContentBody">
|
||||
<ul class="nav nav-tabs">
|
||||
<li
|
||||
class="kbn-settings-tab"
|
||||
ng-class="{ active: ui.currentTab === tab.index }"
|
||||
ng-repeat="tab in ui.tabs"
|
||||
ng-hide="ui.tabs[{{tab.index}}].hidden">
|
||||
|
||||
<a ng-click="ui.changeTab(tab)">
|
||||
{{ tab.title }}
|
||||
<i ng-hide='ui.validation.tabs[tab.index].valid' class='validation-error fa fa-exclamation-circle' />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- tab 0 Job Details -->
|
||||
<ml-job-tab-0 class="tab" ng-show="ui.currentTab === 0">
|
||||
<div class="tab_contents">
|
||||
<!-- ID -->
|
||||
<div class="form-group">
|
||||
<ml-form-label label-id="new_job_id" tooltip-append-to-body="true">
|
||||
{{ ::'xpack.ml.newJob.advanced.jobDetails.nameLabel' | i18n: {defaultMessage: 'Name'} }}
|
||||
</ml-form-label>
|
||||
<input
|
||||
aria-labelledby="ml_aria_label_new_job_id"
|
||||
aria-describedby="ml_aria_description_new_job_id"
|
||||
ng-model="job.job_id"
|
||||
required
|
||||
placeholder="{{ ::'xpack.ml.newJob.advanced.jobDetails.jobIdPlaceholder' | i18n: {defaultMessage: 'Job ID'} }}"
|
||||
ng-change="changeJobIDCase()"
|
||||
input-focus
|
||||
class="form-control lowercase" />
|
||||
<div
|
||||
ng-hide="ui.validation.tabs[0].checks.jobId.valid"
|
||||
class="validation-error"
|
||||
>{{ ( ui.validation.tabs[0].checks.jobId.message || enterJobNameLabel ) }}</div>
|
||||
</div>
|
||||
<!-- description -->
|
||||
<div class="form-group">
|
||||
<ml-form-label label-id="new_job_description">
|
||||
{{ ::'xpack.ml.newJob.advanced.jobDetails.descriptionLabel' | i18n: {defaultMessage: 'Description'} }}
|
||||
</ml-form-label>
|
||||
<input
|
||||
aria-labelledby="ml_aria_label_new_job_description"
|
||||
aria-describedby="ml_aria_description_new_job_description"
|
||||
ng-model="job.description"
|
||||
placeholder="{{ ::'xpack.ml.newJob.advanced.jobDetails.jobDescriptionPlaceholder' | i18n: {defaultMessage: 'Job description'} }}"
|
||||
class="form-control" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<ml-form-label label-id="new_job_group">
|
||||
{{ ::'xpack.ml.newJob.advanced.jobDetails.jobGroupsLabel' | i18n: {defaultMessage: 'Job Groups'} }}
|
||||
</ml-form-label>
|
||||
<ml-job-group-select
|
||||
aria-labelledby="ml_aria_label_new_job_group"
|
||||
aria-describedby="ml_aria_description_new_job_group"
|
||||
job-groups='job.groups'
|
||||
external-update-function='jobGroupsUpdateFunction' />
|
||||
<div ng-hide="ui.validation.checks.groupIds.valid" class="validation-error">{{ ui.validation.tabs[0].checks.groupIds.message }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label
|
||||
class="kuiFormLabel"
|
||||
i18n-id="xpack.ml.newJob.advanced.jobDetails.customUrlsLabel"
|
||||
i18n-default-message="Custom URLs"
|
||||
></label><i ml-info-icon="new_job_custom_urls" />
|
||||
<div class="euiSpacer euiSpacer--s"></div>
|
||||
<div ng-if="job.custom_settings && job.custom_settings.custom_urls">
|
||||
<div ng-repeat="item in job.custom_settings.custom_urls track by $index" class="custom-url">
|
||||
<div class="field-cols">
|
||||
|
||||
<div class="form-group">
|
||||
<label
|
||||
class="kuiFormLabel"
|
||||
id="ml_aria_label_custom_url_label_{{$index}}"
|
||||
i18n-id="xpack.ml.newJob.advanced.jobDetails.labelLabel"
|
||||
i18n-default-message="Label"
|
||||
></label>
|
||||
<input
|
||||
aria-labelledby="ml_aria_label_custom_url_label_{{$index}}"
|
||||
ng-model="item.url_name"
|
||||
type="text"
|
||||
class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-cols">
|
||||
<div class="form-group">
|
||||
<label
|
||||
class="kuiFormLabel"
|
||||
id="ml_aria_label_custom_url_{{$index}}"
|
||||
i18n-id="xpack.ml.newJob.advanced.jobDetails.urlLabel"
|
||||
i18n-default-message="URL"
|
||||
></label>
|
||||
<textarea
|
||||
aria-labelledby="ml_aria_label_custom_url_{{$index}}"
|
||||
ng-model="item.url_value"
|
||||
type="text"
|
||||
class="form-control" ></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-label="{{ ::'xpack.ml.newJob.advanced.jobDetails.removeCustomUrlButtonAriaLabel' | i18n: {defaultMessage: 'Remove Custom URL'} }}"
|
||||
ng-click="removeCustomUrl($index)"
|
||||
tooltip-append-to-body="true"
|
||||
type="button"
|
||||
class="kuiButton kuiButton--danger kuiButton--small remove-button">
|
||||
<i aria-hidden="true" class="fa fa-times" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
aria-labelledby="ml_aria_label_new_job_custom_urls"
|
||||
aria-describedby="ml_aria_description_new_job_custom_urls"
|
||||
ng-click="addCustomUrl()"
|
||||
type="button"
|
||||
class="kuiButton kuiButton--primary kuiButton--small">
|
||||
<i aria-hidden="true" class="fa fa-plus" />
|
||||
<span
|
||||
id="ml_aria_label_new_job_custom_urls"
|
||||
i18n-id="xpack.ml.newJob.advanced.jobDetails.addCustomUrlButtonLabel"
|
||||
i18n-default-message="Add Custom URL"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class='kuiCheckBoxLabel kuiVerticalRhythm'>
|
||||
<input type="checkbox"
|
||||
aria-labelledby="ml_aria_label_new_job_dedicated_index"
|
||||
aria-describedby="ml_aria_description_new_job_dedicated_index"
|
||||
class='kuiCheckBox'
|
||||
ng-change="setDedicatedIndex()"
|
||||
ng-model ="ui.useDedicatedIndex" />
|
||||
<span class='kuiCheckBoxLabel__text'>
|
||||
<span
|
||||
id="ml_aria_label_new_job_dedicated_index"
|
||||
i18n-id="xpack.ml.newJob.advanced.jobDetails.useDedicatedIndexLabel"
|
||||
i18n-default-message="Use dedicated index"
|
||||
></span>
|
||||
<i ml-info-icon="new_job_dedicated_index" />
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<ml-form-label label-id="new_job_model_memory_limit">
|
||||
{{ ::'xpack.ml.newJob.advanced.jobDetails.modelMemoryLimitLabel' | i18n: {defaultMessage: 'Model memory limit'} }}
|
||||
</ml-form-label>
|
||||
<input
|
||||
aria-labelledby="ml_aria_label_new_job_model_memory_limit"
|
||||
aria-describedby="ml_aria_description_new_job_model_memory_limit"
|
||||
ng-model="ui.modelMemoryLimitText"
|
||||
placeholder="{{ui.modelMemoryLimitDefault}}"
|
||||
class="form-control" />
|
||||
<div ng-hide="ui.validation.tabs[0].checks.modelMemoryLimit.valid" class="validation-error">{{ ui.validation.tabs[0].checks.modelMemoryLimit.message }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ml-job-tab-0>
|
||||
|
||||
<!-- tab2 1 Analysis Configuration -->
|
||||
<ml-job-tab-1 ng-show="ui.currentTab === 1">
|
||||
<div class="tab_contents">
|
||||
<div class="form-group">
|
||||
<ml-form-label label-id="new_job_bucketspan">bucket_span</ml-form-label>
|
||||
<input
|
||||
aria-labelledby="ml_aria_label_new_job_bucketspan"
|
||||
aria-describedby="ml_aria_description_new_job_bucketspan"
|
||||
type="text"
|
||||
ng-model="job.analysis_config.bucket_span"
|
||||
placeholder=""
|
||||
ng-change="calculateDatafeedFrequencyDefaultSeconds()"
|
||||
class="form-control" />
|
||||
<div ng-hide="ui.validation.tabs[1].checks.bucketSpan.valid" class="validation-error">
|
||||
{{ ( ui.validation.tabs[1].checks.bucketSpan.message || bucketSpanNotValidFormatLabel ) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<ml-form-label label-id="new_job_summarycountfieldname">summary_count_field_name</ml-form-label>
|
||||
<field-select
|
||||
label-id='"new_job_summarycountfieldname"'
|
||||
on-change='setAnalysisConfigProperty'
|
||||
value='job.analysis_config.summary_count_field_name'
|
||||
field='"summary_count_field_name"'
|
||||
options='fields'>
|
||||
</field-select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<ml-form-label label-id="new_job_categorizationfieldname">categorization_field_name</ml-form-label>
|
||||
<field-select
|
||||
label-id='"new_job_categorizationfieldname"'
|
||||
on-change='setAnalysisConfigProperty'
|
||||
value='job.analysis_config.categorization_field_name'
|
||||
field='"categorization_field_name"'
|
||||
options='catFields'>
|
||||
</field-select>
|
||||
</div>
|
||||
|
||||
<div class="form-group"
|
||||
ng-show="(job.analysis_config.categorization_field_name !== undefined && job.analysis_config.categorization_field_name !== '') ||
|
||||
(job.analysis_config.categorization_filters && job.analysis_config.categorization_filters.length)">
|
||||
|
||||
<label
|
||||
class="kuiFormLabel"
|
||||
aria-describedby="ml_aria_description_new_job_categorizationfilters"
|
||||
i18n-id="xpack.ml.newJob.advanced.analysisConfiguration.categorizationFiltersLabel"
|
||||
i18n-default-message="Categorization Filters"
|
||||
></label>
|
||||
<i ml-info-icon="new_job_categorizationfilters" />
|
||||
<div class="euiSpacer euiSpacer--s"></div>
|
||||
<div ng-if="job.analysis_config && job.analysis_config.categorization_filters">
|
||||
<div ng-repeat="item in job.analysis_config.categorization_filters track by $index" class="categorization-filter">
|
||||
<div class="field-cols">
|
||||
|
||||
<div class="form-group">
|
||||
<input
|
||||
aria-label="{{ ::'xpack.ml.newJob.advanced.analysisConfiguration.categorizationFilterRegularExpressionAriaLabel' | i18n: {defaultMessage: 'Categorization filter regular expression'} }}"
|
||||
ng-model="job.analysis_config.categorization_filters[$index]"
|
||||
type="text"
|
||||
class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-label="{{ ::'xpack.ml.newJob.advanced.analysisConfiguration.removeCategorizationFilterButtonAriaLabel' | i18n: {defaultMessage: 'Remove categorization filter'} }}"
|
||||
ng-click="removeCategorizationFilter($index)"
|
||||
tooltip-append-to-body="true"
|
||||
type="button"
|
||||
class="kuiButton kuiButton--danger kuiButton--small remove-button">
|
||||
<i aria-hidden="true" class="fa fa-times" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
aria-labelledby="ml_aria_label_add_categorization_filter"
|
||||
ng-click="addCategorizationFilter()"
|
||||
type="button"
|
||||
ng-disabled="job.analysis_config.categorization_field_name === undefined || job.analysis_config.categorization_field_name === ''"
|
||||
class="kuiButton kuiButton--primary kuiButton--small">
|
||||
<i aria-hidden="true" class="fa fa-plus" />
|
||||
<span
|
||||
id="ml_aria_label_add_categorization_filter"
|
||||
i18n-id="xpack.ml.newJob.advanced.analysisConfiguration.addCategorizationFilterButtonLabel"
|
||||
i18n-default-message="Add Categorization Filter"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-hide="ui.validation.tabs[1].checks.categorizationFilters.valid" class="validation-error">
|
||||
{{ ( ui.validation.tabs[1].checks.categorizationFilters.message || categorizationFiltersNotValidLabel ) }}
|
||||
</div>
|
||||
|
||||
<hr class="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginMedium">
|
||||
<label
|
||||
class="kuiFormLabel"
|
||||
aria-describedby="ml_aria_description_new_job_detectors"
|
||||
i18n-id="xpack.ml.newJob.advanced.analysisConfiguration.detectorsLabel"
|
||||
i18n-default-message="Detectors"
|
||||
></label>
|
||||
<i ml-info-icon="new_job_detectors" />
|
||||
|
||||
<div class="euiSpacer euiSpacer--s"></div>
|
||||
|
||||
<div ml-job-detectors-list
|
||||
ml-detectors="job.analysis_config.detectors"
|
||||
ml-indices="indices"
|
||||
ml-fields="fields"
|
||||
ml-cat-field-name-selected="(job.analysis_config.categorization_field_name?true:false)"
|
||||
ml-edit-mode="'NEW'"
|
||||
ml-on-detectors-update="onDetectorsUpdate"
|
||||
></div>
|
||||
<div ng-hide="ui.validation.tabs[1].checks.detectors.valid" class="validation-error">
|
||||
{{ ( ui.validation.tabs[1].checks.detectors.message || detectorNotConfiguredLabel ) }}
|
||||
</div>
|
||||
|
||||
<hr class="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginMedium">
|
||||
|
||||
<label
|
||||
class="kuiFormLabel"
|
||||
aria-describedby="ml_aria_description_new_job_influencers"
|
||||
i18n-id="xpack.ml.newJob.advanced.analysisConfiguration.influencersLabel"
|
||||
i18n-default-message="Influencers"
|
||||
></label>
|
||||
<i ml-info-icon="new_job_influencers" />
|
||||
<div class="influencer-list-container">
|
||||
|
||||
<div ng-repeat="inf in ui.allInfluencers()" >
|
||||
<label class='kuiCheckBoxLabel kuiVerticalRhythm'>
|
||||
<input class='kuiCheckBox' type="checkbox" ng-checked="influencerChecked(inf)" ng-click="toggleInfluencer(inf)" />
|
||||
<span class='kuiCheckBoxLabel__text'>{{inf}}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="custom-influencer">
|
||||
<input
|
||||
type="text"
|
||||
ng-model="ui.tempCustomInfluencer"
|
||||
placeholder="{{ ::'xpack.ml.newJob.advanced.analysisConfiguration.customInfluencerPlaceholder' | i18n: {defaultMessage: 'Custom influencer'} }}"
|
||||
class="form-control" />
|
||||
<button
|
||||
aria-label="{{ ::'xpack.ml.newJob.advanced.analysisConfiguration.addCustomInfluencerButtonAriaLabel' | i18n: {defaultMessage: 'Add Custom Influencer'} }}"
|
||||
ng-click="addCustomInfluencer()"
|
||||
ng-disabled="ui.tempCustomInfluencer===''"
|
||||
type="button"
|
||||
class="kuiButton kuiButton--primary kuiButton--small"
|
||||
i18n-id="xpack.ml.newJob.advanced.analysisConfiguration.addLabel"
|
||||
i18n-default-message="{icon} Add"
|
||||
i18n-values="{ html_icon: '<i aria-hidden=\'true\' class=\'fa fa-plus\' />' }"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-hide="ui.validation.tabs[1].checks.influencers.valid" class="validation-error">
|
||||
{{ ( ui.validation.tabs[1].checks.influencers.message || influencerNotSelectedLabel ) }}
|
||||
</div>
|
||||
|
||||
<hr class="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginMedium">
|
||||
|
||||
<div class="form-group">
|
||||
<label class='kuiCheckBoxLabel kuiVerticalRhythm'>
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-labelledby="ml_aria_label_new_job_enable_model_plot"
|
||||
aria-describedby="ml_aria_description_new_job_enable_model_plot"
|
||||
class='kuiCheckBox'
|
||||
ng-change="setModelPlotEnabled()"
|
||||
ng-model="ui.enableModelPlot" />
|
||||
<span class='kuiCheckBoxLabel__text'>
|
||||
<span id="ml_aria_label_new_job_enable_model_plot">
|
||||
{{ ui.cardinalityValidator.status === ui.cardinalityValidator.STATUS.RUNNING ? validatingCardinalityLabel : enableModelPlotLabel }}
|
||||
</span>
|
||||
<i ml-info-icon="new_job_enable_model_plot" />
|
||||
</span>
|
||||
</label>
|
||||
<div class='ml-new-job-callout kuiVerticalRhythm'>
|
||||
<ml-enable-model-plot-callout
|
||||
message='ui.cardinalityValidator.message'
|
||||
ng-show="ui.cardinalityValidator.status === ui.cardinalityValidator.STATUS.WARNING ||
|
||||
ui.cardinalityValidator.status === ui.cardinalityValidator.STATUS.FAILED">
|
||||
</ml-enable-model-plot-callout>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ml-job-tab-1>
|
||||
|
||||
|
||||
<!-- tab 2 Data Description -->
|
||||
<ml-job-tab-2 ng-show="ui.currentTab === 2">
|
||||
<div class="tab_contents">
|
||||
<div class="form-group">
|
||||
<ml-form-label label-id="new_job_data_format">
|
||||
{{ ::'xpack.ml.newJob.advanced.dataDescription.dataFormatLabel' | i18n: {defaultMessage: 'Data format'} }}
|
||||
</ml-form-label>
|
||||
<select
|
||||
aria-labelledby="ml_aria_label_new_job_data_format"
|
||||
aria-describedby="ml_aria_description_new_job_data_format"
|
||||
ng-model="job.data_description.format"
|
||||
ng-disabled="ui.isDatafeed"
|
||||
ng-options="item.value as item.title for item in ui.inputDataFormat"
|
||||
class="form-control">
|
||||
</select>
|
||||
</div>
|
||||
<ml-job-delimited-options ng-show="job.data_description.format==='delimited'">
|
||||
<div class="form-group">
|
||||
<ml-form-label label-id="new_job_delimiter">
|
||||
{{ ::'xpack.ml.newJob.advanced.dataDescription.delimiterLabel' | i18n: {defaultMessage: 'Delimiter'} }}
|
||||
</ml-form-label>
|
||||
<select
|
||||
aria-labelledby="ml_aria_label_new_job_delimiter"
|
||||
aria-describedby="ml_aria_description_new_job_delimiter"
|
||||
ng-model="ui.selectedFieldDelimiter"
|
||||
ng-options="item.value as item.title for item in ui.fieldDelimiterOptions"
|
||||
class="form-control" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input
|
||||
ng-model="ui.customFieldDelimiter"
|
||||
ng-show="ui.selectedFieldDelimiter==='custom'"
|
||||
ng-required="job.data_description.format==='delimited' && ui.selectedFieldDelimiter==='custom'"
|
||||
class="form-control" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<ml-form-label label-id="new_job_quote_character">
|
||||
{{ ::'xpack.ml.newJob.advanced.dataDescription.quoteCharacterLabel' | i18n: {defaultMessage: 'Quote character'} }}
|
||||
</ml-form-label>
|
||||
<input
|
||||
aria-labelledby="ml_aria_label_new_job_quote_character"
|
||||
aria-describedby="ml_aria_description_new_job_quote_character"
|
||||
ng-model="job.data_description.quote_character"
|
||||
ng-required="job.data_description.format==='delimited'"
|
||||
placeholder=""
|
||||
class="form-control" />
|
||||
</div>
|
||||
</ml-job-delimited-options>
|
||||
|
||||
<div class="form-group">
|
||||
<ml-form-label label-id="new_job_time_field">
|
||||
{{ ::'xpack.ml.newJob.advanced.dataDescription.timeFieldLabel' | i18n: {defaultMessage: 'Time field'} }}
|
||||
</ml-form-label>
|
||||
<input
|
||||
aria-labelledby="ml_aria_label_new_job_time_field"
|
||||
aria-describedby="ml_aria_description_new_job_time_field"
|
||||
ng-model="job.data_description.time_field"
|
||||
required
|
||||
placeholder=""
|
||||
class="form-control" />
|
||||
<div ng-hide="ui.validation.tabs[2].checks.timeField.valid" class="validation-error">
|
||||
{{ ( ui.validation.tabs[2].checks.timeField.message || specifyTimeFieldLabel ) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<ml-form-label label-id="new_job_time_format">
|
||||
{{ ::'xpack.ml.newJob.advanced.dataDescription.timeFormatLabel' | i18n: {defaultMessage: 'Time format'} }}
|
||||
</ml-form-label>
|
||||
<input
|
||||
aria-labelledby="ml_aria_label_new_job_time_format"
|
||||
aria-describedby="ml_aria_description_new_job_time_format"
|
||||
ng-model="job.data_description.time_format"
|
||||
required
|
||||
placeholder=""
|
||||
class="form-control" />
|
||||
<div ng-hide="ui.validation.tabs[2].checks.timeFormat.valid" class="validation-error">
|
||||
{{ ( ui.validation.tabs[2].checks.timeFormat.message || specifyTimeFormatLabel ) }}
|
||||
</div>
|
||||
<div
|
||||
ng-if="exampleTime"
|
||||
class="time-example"
|
||||
i18n-id="xpack.ml.newJob.advanced.dataDescription.exampleTimeDescription"
|
||||
i18n-default-message="e.g. {exampleTime}"
|
||||
i18n-values="{ exampleTime }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</ml-job-tab-2>
|
||||
|
||||
<!-- tab 3 Datafeed -->
|
||||
<ml-job-tab-3 ng-show="ui.currentTab === 3">
|
||||
<div class="tab_contents">
|
||||
<label class='kuiCheckBoxLabel kuiVerticalRhythm'>
|
||||
<input
|
||||
aria-labelledby="ml_aria_label_new_job_enable_datafeed_job"
|
||||
aria-describedby="ml_aria_description_new_job_enable_datafeed_job"
|
||||
class='kuiCheckBox'
|
||||
ng-model="ui.isDatafeed"
|
||||
ng-change="datafeedChange()"
|
||||
ng-disabled="job.data_description.format!=='json'"
|
||||
type="checkbox" />
|
||||
<span class='kuiCheckBoxLabel__text'>
|
||||
<span
|
||||
id="ml_aria_label_new_job_enable_datafeed_job"
|
||||
i18n-id="xpack.ml.newJob.advanced.datafeed.datafeedJobLabel"
|
||||
i18n-default-message="Datafeed job"
|
||||
></span>
|
||||
<i ml-info-icon="new_job_enable_datafeed_job" />
|
||||
</span>
|
||||
</label>
|
||||
<div class="euiSpacer euiSpacer--s"></div>
|
||||
<div class="form-group help-pane" ng-show="job.data_description.format!=='json' && job.data_description.format!==undefined">
|
||||
<small
|
||||
class="info"
|
||||
i18n-id="xpack.ml.newJob.advanced.datafeed.enableDatafeedDescription"
|
||||
i18n-default-message="Data format must be set to 'JSON' to enable the datafeed."
|
||||
></small>
|
||||
</div>
|
||||
|
||||
<div ng-if="ui.isDatafeed">
|
||||
<div class="form-group">
|
||||
<ml-form-label label-id="new_job_datafeed_query" tooltip-append-to-body="true">
|
||||
{{ ::'xpack.ml.newJob.advanced.datafeed.queryLabel' | i18n: {defaultMessage: 'Query'} }}
|
||||
</ml-form-label>
|
||||
<input
|
||||
aria-labelledby="ml_aria_label_new_job_datafeed_query"
|
||||
aria-describedby="ml_aria_description_new_job_datafeed_query"
|
||||
ng-model="ui.datafeed.queryText"
|
||||
placeholder='{ "match_all": {}}'
|
||||
class="form-control" />
|
||||
|
||||
</div>
|
||||
<div class="form-group" >
|
||||
<ml-form-label label-id="new_job_datafeed_query_delay">
|
||||
{{ ::'xpack.ml.newJob.advanced.datafeed.queryDelayLabel' | i18n: {defaultMessage: 'Query delay'} }}
|
||||
</ml-form-label>
|
||||
<input
|
||||
aria-labelledby="ml_aria_label_new_job_datafeed_query_delay"
|
||||
aria-describedby="ml_aria_description_new_job_datafeed_query_delay"
|
||||
ng-model="ui.datafeed.queryDelayText"
|
||||
placeholder="{{ui.datafeed.queryDelayDefault}}"
|
||||
min="0"
|
||||
class="form-control" />
|
||||
|
||||
</div>
|
||||
<div class="form-group" >
|
||||
<ml-form-label label-id="new_job_datafeed_frequency">
|
||||
{{ ::'xpack.ml.newJob.advanced.datafeed.frequencyLabel' | i18n: {defaultMessage: 'Frequency'} }}
|
||||
</ml-form-label>
|
||||
<input
|
||||
aria-labelledby="ml_aria_label_new_job_datafeed_frequency"
|
||||
aria-describedby="ml_aria_description_new_job_datafeed_frequency"
|
||||
ng-model="ui.datafeed.frequencyText"
|
||||
placeholder="{{ui.datafeed.frequencyDefault}}"
|
||||
min="0"
|
||||
class="form-control" />
|
||||
|
||||
</div>
|
||||
<div class="form-group" >
|
||||
<ml-form-label label-id="new_job_datafeed_scrollsize" tooltip-append-to-body="true">scroll_size</ml-form-label>
|
||||
<input
|
||||
aria-labelledby="ml_aria_label_new_job_datafeed_scrollsize"
|
||||
aria-describedby="ml_aria_description_new_job_datafeed_scrollsize"
|
||||
ng-model="ui.datafeed.scrollSizeText"
|
||||
placeholder="{{ui.datafeed.scrollSizeDefault}}"
|
||||
type="number"
|
||||
min="0"
|
||||
class="form-control" />
|
||||
|
||||
</div>
|
||||
<div class="form-group" >
|
||||
<div class="form-group">
|
||||
<label
|
||||
class="kuiFormLabel"
|
||||
i18n-id="xpack.ml.newJob.advanced.datafeed.indexLabel"
|
||||
i18n-default-message="Index"
|
||||
></label>
|
||||
<div class="input-group">
|
||||
<input
|
||||
ng-model="ui.datafeed.indicesText"
|
||||
placeholder=""
|
||||
class="form-control"
|
||||
aria-describedby="index-text-status"
|
||||
ng-change="indexChanged()"
|
||||
list='index_datalist' />
|
||||
<span class="input-group-addon" id="index-text-status">
|
||||
<i ng-show="ui.indexTextOk === true && ui.fieldsUpToDate === true" aria-hidden="true" style='color:green;' class="fa fa-check"></i>
|
||||
<i ng-show="ui.indexTextOk === false || ui.fieldsUpToDate === false" aria-hidden="true" style='color:red;' class="fa fa-remove"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div ng-hide="ui.validation.tabs[3].checks.hasAccessToIndex.valid" class="validation-error">{{ ( ui.validation.tabs[3].checks.hasAccessToIndex.message) }}</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="ui.fieldsUpToData === false || ui.fieldsUpToDate === false">
|
||||
<button
|
||||
ng-click="loadFields()"
|
||||
type="button"
|
||||
class="kuiButton kuiButton--primary kuiButton--small">
|
||||
<i aria-hidden="true" class="fa fa-refresh"></i>
|
||||
<span
|
||||
i18n-id="xpack.ml.newJob.advanced.datafeed.reloadIndexButtonLabel"
|
||||
i18n-default-message="Reload index"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div ng-show="ui.indexTextOk && ui.fieldsUpToDate === true" class="form-group">
|
||||
<label
|
||||
class="kuiFormLabel"
|
||||
i18n-id="xpack.ml.newJob.advanced.datafeed.timeFieldNameLabel"
|
||||
i18n-default-message="Time-field name"
|
||||
></label>
|
||||
<select
|
||||
ng-model="job.data_description.time_field"
|
||||
class="form-control">
|
||||
<option ng-repeat="(key, value) in dateFields">{{key}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ml-job-tab-3>
|
||||
|
||||
<!-- tab 4 Edit JSON -->
|
||||
<ml-job-tab-4 ng-show="ui.currentTab === 4" class="ml_json_tab">
|
||||
<div class="tab_contents">
|
||||
<label
|
||||
class="kuiFormLabel"
|
||||
id="ml_aria_label_new_job_json"
|
||||
i18n-id="xpack.ml.newJob.advanced.json.jsonLabel"
|
||||
i18n-default-message="JSON"
|
||||
></label>
|
||||
<div
|
||||
class="form-control json-textarea"
|
||||
ui-ace="{
|
||||
mode: 'json',
|
||||
onChange: jsonTextChange
|
||||
}"
|
||||
ng-model="ui.jsonText"
|
||||
></div>
|
||||
</div>
|
||||
</ml-job-tab-4>
|
||||
|
||||
<!-- tab 5 Data preview -->
|
||||
<ml-job-tab-5 ng-show="ui.currentTab === 5" class="ml_data_preview_tab">
|
||||
<div class="tab_contents">
|
||||
<ml-form-label label-id="new_job_data_preview">
|
||||
{{ ::'xpack.ml.newJob.advanced.dataPreview.dataPreviewLabel' | i18n: {defaultMessage: 'Data preview'} }}
|
||||
</ml-form-label>
|
||||
<ml-loading-indicator
|
||||
label="{{ ::'xpack.ml.newJob.advanced.dataPreview.loadingDataPreviewLabel' | i18n: {defaultMessage: 'Loading data preview'} }}"
|
||||
is-loading="(ui.dataPreview === '')"
|
||||
/>
|
||||
<div ng-hide="(ui.dataPreview === '')">
|
||||
<div
|
||||
id="datafeed-preview"
|
||||
class="form-control json-textarea"
|
||||
ui-ace="{
|
||||
mode: 'json',
|
||||
onLoad: aceLoaded
|
||||
}"
|
||||
ng-model="ui.dataPreview"
|
||||
></div>
|
||||
<div
|
||||
class="note"
|
||||
i18n-id="xpack.ml.newJob.advanced.dataPreview.previewContentReturnedDescription"
|
||||
i18n-default-message="Preview returns the content of the {source} field only."
|
||||
i18n-values="{ source: '_source' }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</ml-job-tab-5>
|
||||
|
||||
<hr class="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginMedium">
|
||||
|
||||
<div class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--alignItemsCenter euiFlexGroup--responsive">
|
||||
<div class="euiFlexItem euiFlexItem--flexGrowZero">
|
||||
<ml-validate-job
|
||||
fields="fields"
|
||||
fill="false"
|
||||
get-job-config="getJobConfig"
|
||||
is-current-job-config="isCurrentJobConfig"
|
||||
is-disabled="(saveLock === true)"
|
||||
ng-show="jobState === JOB_STATE.NOT_STARTED"
|
||||
/>
|
||||
</div>
|
||||
<div class="euiFlexItem euiFlexItem--flexGrowZero">
|
||||
<button
|
||||
ng-click="save()"
|
||||
ng-disabled="(saveLock === true)"
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill"
|
||||
aria-label="{{ ::'xpack.ml.newJob.advanced.saveButtonAriaLabel' | i18n: {defaultMessage: 'Save'} }}">
|
||||
<span class="euiButton__content">
|
||||
<span
|
||||
i18n-id="xpack.ml.newJob.advanced.saveButtonLabel"
|
||||
i18n-default-message="Save"
|
||||
></span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="euiFlexItem euiFlexItem--flexGrowZero">
|
||||
<button
|
||||
ng-click="cancel()"
|
||||
ng-disabled="(saveLock === true)"
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill"
|
||||
aria-label="{{ ::'xpack.ml.newJob.advanced.cancelButtonAriaLabel' | i18n: {defaultMessage: 'Cancel'} }}">
|
||||
<span class="euiButton__content">
|
||||
<span
|
||||
i18n-id="xpack.ml.newJob.advanced.cancelButtonLabel"
|
||||
i18n-default-message="Cancel"
|
||||
></span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ml-new-job>
|
File diff suppressed because it is too large
Load diff
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import ngMock from 'ng_mock';
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
const mockModalInstance = { close: function () { }, dismiss: function () { } };
|
||||
|
||||
describe('ML - Save Status Modal Controller', () => {
|
||||
beforeEach(() => {
|
||||
ngMock.module('kibana');
|
||||
});
|
||||
|
||||
it('Initialize Save Status Modal Controller', (done) => {
|
||||
ngMock.inject(function ($rootScope, $controller) {
|
||||
const scope = $rootScope.$new();
|
||||
|
||||
expect(() => {
|
||||
$controller('MlSaveStatusModal', {
|
||||
$scope: scope,
|
||||
$modalInstance: mockModalInstance,
|
||||
params: {}
|
||||
});
|
||||
}).to.not.throwError();
|
||||
|
||||
expect(scope.ui.showTimepicker).to.eql(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1 +0,0 @@
|
|||
@import 'save_status_modal';
|
|
@ -1,14 +0,0 @@
|
|||
.save-status-modal {
|
||||
padding: $euiSizeL;
|
||||
cursor: auto;
|
||||
|
||||
// SASSTODO: Proper selector
|
||||
h3 {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
padding-top: $euiSizeS;
|
||||
font-weight: $euiFontWeightBold;
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import './save_status_modal_controller';
|
|
@ -1,38 +0,0 @@
|
|||
<div class="save-status-modal">
|
||||
<!-- <ml-message-bar ></ml-message-bar> -->
|
||||
<h3
|
||||
class="euiTitle euiTitle--small"
|
||||
i18n-id="xpack.ml.newJob.advanced.saveStatusModal.savingNewJobTitle"
|
||||
i18n-default-message="Saving new job"
|
||||
></h3>
|
||||
<div class="status-item">
|
||||
<span
|
||||
i18n-id="xpack.ml.newJob.advanced.saveStatusModal.savingJobLabel"
|
||||
i18n-default-message="Saving job…"
|
||||
></span>
|
||||
<i ng-show="pscope.ui.saveStatus.job === -1" aria-hidden="true" style="color:red;" class="fa fa-remove"></i>
|
||||
<i ng-show="pscope.ui.saveStatus.job === 1" aria-hidden="true" class="fa fa-spinner fa-spin"></i>
|
||||
<i ng-show="pscope.ui.saveStatus.job === 2" aria-hidden="true" style="color:green;" class="fa fa-check"></i>
|
||||
</div>
|
||||
|
||||
<hr class="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginMedium">
|
||||
|
||||
<button
|
||||
ng-show="(pscope.ui.saveStatus.job === 2 && pscope.ui.isDatafeed)"
|
||||
ng-disabled="pscope.saveLock"
|
||||
ng-click="openDatafeed();"
|
||||
class="kuiButton kuiButton--primary"
|
||||
aria-label="{{ ::'xpack.ml.newJob.advanced.saveStatusModal.startDatafeedButtonAriaLabel' | i18n: {defaultMessage: 'Back'} }}"
|
||||
i18n-id="xpack.ml.newJob.advanced.saveStatusModal.startDatafeedButtonLabel"
|
||||
i18n-default-message="Start datafeed"
|
||||
></button>
|
||||
|
||||
<button
|
||||
ng-disabled="pscope.saveLock"
|
||||
ng-click="close();"
|
||||
class="kuiButton kuiButton--primary"
|
||||
aria-label="{{ ::'xpack.ml.newJob.advanced.saveStatusModal.closeButtonAriaLabel' | i18n: {defaultMessage: 'Back'} }}"
|
||||
i18n-id="xpack.ml.newJob.advanced.saveStatusModal.closeButtonLabel"
|
||||
i18n-default-message="Close"
|
||||
></button>
|
||||
</div>
|
|
@ -1,36 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml');
|
||||
|
||||
module.controller('MlSaveStatusModal', function ($scope, $location, $modalInstance, params) {
|
||||
|
||||
$scope.pscope = params.pscope;
|
||||
$scope.ui = {
|
||||
showTimepicker: false,
|
||||
};
|
||||
|
||||
// return to jobs list page and open the datafeed modal for the new job
|
||||
$scope.openDatafeed = function () {
|
||||
$location.path('jobs');
|
||||
$modalInstance.close();
|
||||
params.openDatafeed();
|
||||
};
|
||||
|
||||
// once the job is saved close modal and return to jobs list
|
||||
$scope.close = function () {
|
||||
if ($scope.pscope.ui.saveStatus.job === 2) {
|
||||
$location.path('jobs');
|
||||
}
|
||||
|
||||
$scope.pscope.ui.saveStatus.job = 0;
|
||||
$modalInstance.close();
|
||||
};
|
||||
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
@import 'components/bucket_span_estimator/index'; // SASSTODO: Needs some rewriting
|
||||
@import 'components/bucket_span_selection/index';
|
||||
@import 'components/event_rate_chart/index'; // SASSTODO: Needs some rewriting
|
||||
@import 'components/fields_selection/index'; // SASSTODO: Needs a rewrite
|
||||
@import 'components/fields_selection_population/index'; // SASSTODO: Needs a rewrite
|
||||
@import 'components/general_job_details/index'; // SASSTODO: Needs a rewrite
|
||||
@import 'components/influencers_selection/index';
|
||||
@import 'components/post_save_options/index';
|
||||
@import 'components/watcher/index'; // SASSTODO: Needs calc changes
|
||||
|
||||
@import 'multi_metric/index'; // SASSTODO: Needs some rewriting
|
||||
@import 'population/index'; // SASSTODO: Needs some rewriting
|
||||
@import 'single_metric/index'; // SASSTODO: Needs some rewriting
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import { ML_JOB_FIELD_TYPES } from 'plugins/ml/../common/constants/field_types';
|
||||
import { EVENT_RATE_COUNT_FIELD } from 'plugins/ml/jobs/new_job/simple/components/constants/general';
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml');
|
||||
|
||||
module.filter('filterAggTypes', function () {
|
||||
return (aggTypes, field) => {
|
||||
return aggTypes.filter(type => {
|
||||
if (field.id === EVENT_RATE_COUNT_FIELD) {
|
||||
if(type.isCountType) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if(!type.isCountType) {
|
||||
if (field.mlType === ML_JOB_FIELD_TYPES.KEYWORD || field.mlType === ML_JOB_FIELD_TYPES.IP) {
|
||||
// keywords and ips can't have the full list of aggregations.
|
||||
// currently limited to Distinct count only
|
||||
if (type.isAggregatableStringType) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
});
|
|
@ -1,9 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import './agg_types_filter';
|
|
@ -1,59 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`BucketSpanEstimator renders the button 1`] = `
|
||||
<div
|
||||
className="bucket-span-estimator"
|
||||
>
|
||||
<EuiToolTip
|
||||
content={
|
||||
<FormattedMessage
|
||||
defaultMessage="Experimental feature for estimating bucket span."
|
||||
id="xpack.ml.newJob.simple.bucketSpanEstimator.estimateBucketSpanButtonTooltip"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
delay="regular"
|
||||
position="bottom"
|
||||
>
|
||||
<EuiButton
|
||||
disabled={false}
|
||||
fill={true}
|
||||
iconSide="right"
|
||||
isLoading={false}
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
>
|
||||
Estimate bucket span
|
||||
</EuiButton>
|
||||
</EuiToolTip>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`BucketSpanEstimator renders the loading button 1`] = `
|
||||
<div
|
||||
className="bucket-span-estimator"
|
||||
>
|
||||
<EuiToolTip
|
||||
content={
|
||||
<FormattedMessage
|
||||
defaultMessage="Experimental feature for estimating bucket span."
|
||||
id="xpack.ml.newJob.simple.bucketSpanEstimator.estimateBucketSpanButtonTooltip"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
delay="regular"
|
||||
position="bottom"
|
||||
>
|
||||
<EuiButton
|
||||
disabled={true}
|
||||
fill={true}
|
||||
iconSide="right"
|
||||
isLoading={true}
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
>
|
||||
Estimating bucket span
|
||||
</EuiButton>
|
||||
</EuiToolTip>
|
||||
</div>
|
||||
`;
|
|
@ -1,15 +0,0 @@
|
|||
// SASSTODO: Proper calcs, this looks to brittle to change
|
||||
.bucket-span-estimator {
|
||||
float: right;
|
||||
margin-right: 5px;
|
||||
margin-top: -27px;
|
||||
|
||||
button.euiButton.euiButton--small {
|
||||
font-size: $euiFontSizeS;
|
||||
height: 22px;
|
||||
|
||||
.euiButton__content {
|
||||
padding: 2px 8px 3px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
@import 'bucket_span_estimator';
|
|
@ -1,152 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { BucketSpanEstimator } from './bucket_span_estimator_view';
|
||||
import { EVENT_RATE_COUNT_FIELD } from 'plugins/ml/jobs/new_job/simple/components/constants/general';
|
||||
import { ml } from 'plugins/ml/services/ml_api_service';
|
||||
|
||||
import { I18nContext } from 'ui/i18n';
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml');
|
||||
|
||||
module.directive('mlBucketSpanEstimator', function () {
|
||||
return {
|
||||
restrict: 'AE',
|
||||
replace: false,
|
||||
scope: {
|
||||
bucketSpanFieldChange: '=',
|
||||
formConfig: '=',
|
||||
jobStateWrapper: '=',
|
||||
JOB_STATE: '=jobState',
|
||||
ui: '=ui',
|
||||
exportedFunctions: '='
|
||||
},
|
||||
link: function ($scope, $element) {
|
||||
const STATUS = {
|
||||
FAILED: -1,
|
||||
NOT_RUNNING: 0,
|
||||
RUNNING: 1,
|
||||
FINISHED: 2
|
||||
};
|
||||
|
||||
const errorHandler = (error) => {
|
||||
console.log('Bucket span could not be estimated', error);
|
||||
$scope.ui.bucketSpanEstimator.status = STATUS.FAILED;
|
||||
$scope.ui.bucketSpanEstimator.message = i18n.translate(
|
||||
'xpack.ml.newJob.simple.bucketSpanEstimator.bucketSpanCouldNotBeEstimatedMessage', {
|
||||
defaultMessage: 'Bucket span could not be estimated'
|
||||
});
|
||||
$scope.$applyAsync();
|
||||
};
|
||||
|
||||
$scope.guessBucketSpan = function () {
|
||||
$scope.ui.bucketSpanEstimator.status = STATUS.RUNNING;
|
||||
$scope.ui.bucketSpanEstimator.message = '';
|
||||
$scope.$applyAsync();
|
||||
|
||||
// we need to create a request object here because $scope.formConfig
|
||||
// includes objects with methods which might break the required
|
||||
// object structure when stringified for the server call
|
||||
const data = {
|
||||
aggTypes: [],
|
||||
duration: {
|
||||
start: $scope.formConfig.start,
|
||||
end: $scope.formConfig.end
|
||||
},
|
||||
fields: [],
|
||||
index: $scope.formConfig.indexPattern.title,
|
||||
query: $scope.formConfig.combinedQuery,
|
||||
splitField: $scope.formConfig.splitField && $scope.formConfig.splitField.name,
|
||||
timeField: $scope.formConfig.timeField
|
||||
};
|
||||
|
||||
if ($scope.formConfig.fields === undefined) {
|
||||
// single metric config
|
||||
const fieldName = ($scope.formConfig.field === null) ? null : $scope.formConfig.field.name;
|
||||
data.fields.push(fieldName);
|
||||
data.aggTypes.push($scope.formConfig.agg.type.dslName);
|
||||
} else {
|
||||
// multi metric config
|
||||
Object.keys($scope.formConfig.fields).map((id) => {
|
||||
const field = $scope.formConfig.fields[id];
|
||||
const fieldName = (field.id === EVENT_RATE_COUNT_FIELD) ? null : field.name;
|
||||
data.fields.push(fieldName);
|
||||
data.aggTypes.push(field.agg.type.dslName);
|
||||
});
|
||||
}
|
||||
|
||||
ml.estimateBucketSpan(data)
|
||||
.then((interval) => {
|
||||
if (interval.error) {
|
||||
errorHandler(interval.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const notify = ($scope.formConfig.bucketSpan !== interval.name);
|
||||
$scope.formConfig.bucketSpan = interval.name;
|
||||
$scope.ui.bucketSpanEstimator.status = STATUS.FINISHED;
|
||||
if (notify && typeof $scope.bucketSpanFieldChange === 'function') {
|
||||
$scope.bucketSpanFieldChange();
|
||||
}
|
||||
$scope.$applyAsync();
|
||||
})
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
// export the guessBucketSpan function so it can be called from outside this directive.
|
||||
// this is used when auto populating the settings from the URL.
|
||||
if ($scope.exportedFunctions !== undefined && typeof $scope.exportedFunctions === 'object') {
|
||||
$scope.exportedFunctions.guessBucketSpan = $scope.guessBucketSpan;
|
||||
}
|
||||
|
||||
// watch for these changes
|
||||
$scope.$watch('formConfig.agg.type', updateButton, true);
|
||||
$scope.$watch('jobStateWrapper.jobState', updateButton, true);
|
||||
$scope.$watch('[ui.showJobInput,ui.formValid,ui.bucketSpanEstimator.status]', updateButton, true);
|
||||
|
||||
function updateButton() {
|
||||
const buttonDisabled = (
|
||||
$scope.ui.showJobInput === false ||
|
||||
$scope.ui.formValid === false ||
|
||||
$scope.formConfig.agg.type === undefined ||
|
||||
$scope.jobStateWrapper.jobState === $scope.JOB_STATE.RUNNING ||
|
||||
$scope.jobStateWrapper.jobState === $scope.JOB_STATE.STOPPING ||
|
||||
$scope.jobStateWrapper.jobState === $scope.JOB_STATE.FINISHED ||
|
||||
$scope.ui.bucketSpanEstimator.status === STATUS.RUNNING
|
||||
);
|
||||
const estimatorRunning = ($scope.ui.bucketSpanEstimator.status === STATUS.RUNNING);
|
||||
const buttonText = (estimatorRunning)
|
||||
? i18n.translate('xpack.ml.newJob.simple.bucketSpanEstimator.estimatingBucketSpanButtonLabel', {
|
||||
defaultMessage: 'Estimating bucket span'
|
||||
})
|
||||
: i18n.translate('xpack.ml.newJob.simple.bucketSpanEstimator.estimateBucketSpanButtonLabel', {
|
||||
defaultMessage: 'Estimate bucket span'
|
||||
});
|
||||
|
||||
const props = {
|
||||
buttonDisabled,
|
||||
estimatorRunning,
|
||||
guessBucketSpan: $scope.guessBucketSpan,
|
||||
buttonText
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
<I18nContext>
|
||||
{React.createElement(BucketSpanEstimator, props)}
|
||||
</I18nContext>,
|
||||
$element[0]
|
||||
);
|
||||
}
|
||||
|
||||
updateButton();
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,46 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiToolTip
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
export function BucketSpanEstimator({ buttonDisabled, buttonText, estimatorRunning, guessBucketSpan }) {
|
||||
return (
|
||||
<div className="bucket-span-estimator">
|
||||
<EuiToolTip
|
||||
content={<FormattedMessage
|
||||
id="xpack.ml.newJob.simple.bucketSpanEstimator.estimateBucketSpanButtonTooltip"
|
||||
defaultMessage="Experimental feature for estimating bucket span."
|
||||
/>}
|
||||
position="bottom"
|
||||
>
|
||||
<EuiButton
|
||||
disabled={buttonDisabled}
|
||||
fill
|
||||
iconSide="right"
|
||||
isLoading={estimatorRunning}
|
||||
onClick={guessBucketSpan}
|
||||
size="s"
|
||||
>
|
||||
{buttonText}
|
||||
</EuiButton>
|
||||
</EuiToolTip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
BucketSpanEstimator.propTypes = {
|
||||
buttonDisabled: PropTypes.bool.isRequired,
|
||||
buttonText: PropTypes.string.isRequired,
|
||||
estimatorRunning: PropTypes.bool.isRequired,
|
||||
guessBucketSpan: PropTypes.func.isRequired
|
||||
};
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import React from 'react';
|
||||
|
||||
import { BucketSpanEstimator } from './bucket_span_estimator_view';
|
||||
|
||||
describe('BucketSpanEstimator', () => {
|
||||
|
||||
test('renders the button', () => {
|
||||
const props = {
|
||||
buttonDisabled: false,
|
||||
estimatorRunning: false,
|
||||
guessBucketSpan: () => { },
|
||||
buttonText: 'Estimate bucket span'
|
||||
};
|
||||
const wrapper = shallowWithIntl(<BucketSpanEstimator {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('renders the loading button', () => {
|
||||
const props = {
|
||||
buttonDisabled: true,
|
||||
estimatorRunning: true,
|
||||
guessBucketSpan: () => { },
|
||||
buttonText: 'Estimating bucket span'
|
||||
};
|
||||
const wrapper = shallowWithIntl(<BucketSpanEstimator {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -1,9 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import './bucket_span_estimator_directive.js';
|
|
@ -1,5 +0,0 @@
|
|||
.bucket-span-selection {
|
||||
.bucket-span-input {
|
||||
float: left;
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
@import 'bucket_span_selector';
|
|
@ -1,47 +0,0 @@
|
|||
<div class='bucket-span-selection'>
|
||||
<h4
|
||||
class="euiTitle euiTitle--small"
|
||||
id="ml_aria_label_new_job_bucketspan"
|
||||
i18n-id="xpack.ml.newJob.simple.bucketSpanSelection.bucketSpanTitle"
|
||||
i18n-default-message="Bucket span"
|
||||
></h4><i ml-info-icon="new_job_bucketspan" />
|
||||
<div class="euiSpacer euiSpacer--s"></div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<div>
|
||||
<input
|
||||
aria-labelledby="ml_aria_label_new_job_bucketspan"
|
||||
aria-describedby="ml_aria_description_new_job_bucketspan"
|
||||
ng-model="formConfig.bucketSpan"
|
||||
required
|
||||
placeholder={{formConfig-bucketSpan}}
|
||||
ng-disabled="ui.formValid === false || jobState === JOB_STATE.RUNNING || jobState === JOB_STATE.STOPPING || jobState === JOB_STATE.FINISHED || ui.bucketSpanEstimator.status===1"
|
||||
ng-change="bucketSpanFieldChange()"
|
||||
ng-class='{"ng-invalid": (!ui.bucketSpanValid)}'
|
||||
class="form-control lowercase bucket-span-input" />
|
||||
|
||||
<ml-bucket-span-estimator
|
||||
bucket-span-field-change="bucketSpanFieldChange"
|
||||
form-config='formConfig'
|
||||
job-state-wrapper='{jobState:jobState}'
|
||||
job-state='JOB_STATE'
|
||||
ui='ui'
|
||||
exported-functions='bucketSpanEstimatorExportedFunctions'>
|
||||
</ml-bucket-span-estimator>
|
||||
|
||||
</div>
|
||||
<div
|
||||
ng-hide="ui.bucketSpanValid"
|
||||
class="validation-error"
|
||||
i18n-id="xpack.ml.newJob.simple.bucketSpanSelection.invalidIntervalFormatLabel"
|
||||
i18n-default-message="Invalid interval format"
|
||||
></div>
|
||||
<div ng-show="ui.bucketSpanEstimator.status===-1" class="validation-error">{{ui.bucketSpanEstimator.message}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import template from './bucket_span_selection.html';
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml');
|
||||
|
||||
module.directive('mlBucketSpanSelection', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
template,
|
||||
controller: function ($scope) {
|
||||
|
||||
$scope.bucketSpanFieldChange = function () {
|
||||
$scope.ui.bucketSpanEstimator.status = 0;
|
||||
$scope.ui.bucketSpanEstimator.message = '';
|
||||
$scope.formChange();
|
||||
};
|
||||
|
||||
// this is passed into the bucketspan estimator and reference to the guessBucketSpan function is inserted
|
||||
// to allow it for be called automatically without user interaction.
|
||||
$scope.bucketSpanEstimatorExportedFunctions = {};
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,9 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import './bucket_span_selection_directive';
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue