[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:
James Gowdy 2019-10-15 21:48:49 +01:00 committed by GitHub
parent 89860fb8fe
commit 53420a9f9d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
188 changed files with 0 additions and 17103 deletions

View file

@ -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';

View file

@ -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();
});
});
});

View file

@ -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;
}
}

View file

@ -1 +0,0 @@
@import 'confirm_modal';

View file

@ -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>

View file

@ -1,41 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* 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();
};
});

View file

@ -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,
};
}
}
});
});
};
});

View file

@ -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';

View file

@ -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>
`;

View file

@ -1,6 +0,0 @@
// SASSTODO: Make a defined selector
.documentation-help-link {
i {
margin-left: $euiSizeXS;
}
}

View file

@ -1 +0,0 @@
@import 'documentation_help_link';

View file

@ -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);
}
};
});

View file

@ -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
};

View file

@ -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();
});
});

View file

@ -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';

View file

@ -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;
}
}
}

View file

@ -1 +0,0 @@
@import 'form_filter_input';

View file

@ -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>

View file

@ -1,41 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* 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' });
}
};
});

View file

@ -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';

View file

@ -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>
`;

View file

@ -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);
});
});

View file

@ -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;
}
}

View file

@ -1 +0,0 @@
@import 'form_label';

View file

@ -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
};

View file

@ -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();
});
});

View file

@ -1,52 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* 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]
);
}
};
});

View file

@ -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';

View file

@ -1 +0,0 @@
@import 'job_group_select';

View file

@ -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;
}

View file

@ -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';

View file

@ -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>

View file

@ -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' });
}
}
}
};
});

View file

@ -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>
`;

View file

@ -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);
});
});

View file

@ -1 +0,0 @@
@import 'json_tooltip';

View file

@ -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
}

View file

@ -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';

View file

@ -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
};

View file

@ -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();
});
});

View file

@ -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]
);
}
};
});

View file

@ -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.'
})
}
};
};

View file

@ -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>

View file

@ -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;
});

View file

@ -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>

View file

@ -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';

View file

@ -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
};
});

View file

@ -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
};

View file

@ -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;
}]);

View file

@ -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';

View file

@ -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

View file

@ -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';

View file

@ -1,2 +0,0 @@
@import 'advanced/index';
@import 'simple/index';

View file

@ -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();
});
});
});

View file

@ -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;
}
}

View file

@ -1,4 +0,0 @@
@import 'advanced';
@import 'detector_filter_modal/index';
@import 'detector_modal/index';
@import 'save_status_modal/index';

View file

@ -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();
});
});
});

View file

@ -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;
}
}

View file

@ -1 +0,0 @@
@import 'detector_filter_modal';

View file

@ -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>

View file

@ -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();
};
});

View file

@ -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';

View file

@ -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();
});
});
});

View file

@ -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;
}
}
}
}

View file

@ -1 +0,0 @@
@import 'detector_modal';

View file

@ -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>

View file

@ -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();
};
});

View file

@ -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';

View file

@ -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>

View file

@ -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
};
}
}
});
};
}
};
});

View file

@ -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);
});
});

View file

@ -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' }
)
);
});

View file

@ -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,
};

View file

@ -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';

View file

@ -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
};

View file

@ -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);
});

View file

@ -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';

View file

@ -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>

View file

@ -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();
});
});
});

View file

@ -1 +0,0 @@
@import 'save_status_modal';

View file

@ -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;
}
}

View file

@ -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';

View file

@ -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>

View file

@ -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();
};
});

View file

@ -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

View file

@ -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;
});
};
});

View file

@ -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';

View file

@ -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>
`;

View file

@ -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;
}
}
}

View file

@ -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();
}
};
});

View file

@ -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
};

View file

@ -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();
});
});

View file

@ -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';

View file

@ -1,5 +0,0 @@
.bucket-span-selection {
.bucket-span-input {
float: left;
}
}

View file

@ -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>

View file

@ -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 = {};
}
};
});

View file

@ -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