diff --git a/x-pack/plugins/ml/public/settings/_index.scss b/x-pack/plugins/ml/public/settings/_index.scss
index fa32ea3cbff34..f29a6e3a192b0 100644
--- a/x-pack/plugins/ml/public/settings/_index.scss
+++ b/x-pack/plugins/ml/public/settings/_index.scss
@@ -1,3 +1,3 @@
@import 'settings';
@import 'filter_lists/index';
-@import 'scheduled_events/index';
\ No newline at end of file
+@import 'calendars/index';
diff --git a/x-pack/plugins/ml/public/settings/calendars/_calendars.scss b/x-pack/plugins/ml/public/settings/calendars/_calendars.scss
new file mode 100644
index 0000000000000..9e60ec241b8e8
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/_calendars.scss
@@ -0,0 +1,4 @@
+.mlCalendarManagement {
+ background: $euiColorLightestShade;
+ min-height: 100vh;
+}
diff --git a/x-pack/plugins/ml/public/settings/calendars/_index.scss b/x-pack/plugins/ml/public/settings/calendars/_index.scss
new file mode 100644
index 0000000000000..7284feef6a46e
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/_index.scss
@@ -0,0 +1,3 @@
+@import 'calendars';
+@import 'edit/index';
+@import 'list/index';
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap b/x-pack/plugins/ml/public/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap
new file mode 100644
index 0000000000000..c1fd683d1c6e0
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap
@@ -0,0 +1,38 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`NewCalendar Renders new calendar form 1`] = `
+
+
+
+
+
+`;
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/_edit.scss b/x-pack/plugins/ml/public/settings/calendars/edit/_edit.scss
new file mode 100644
index 0000000000000..4027f519bc915
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/_edit.scss
@@ -0,0 +1,8 @@
+.mlCalendarEditForm {
+ .mlCalendarEditForm__content {
+ max-width: map-get($euiBreakpoints, 'xl');
+ width: 100%;
+ margin-top: $euiSize;
+ margin-bottom: $euiSize;
+ }
+}
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/_index.scss b/x-pack/plugins/ml/public/settings/calendars/edit/_index.scss
new file mode 100644
index 0000000000000..6928f6ce68281
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/_index.scss
@@ -0,0 +1 @@
+@import 'edit';
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap b/x-pack/plugins/ml/public/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap
new file mode 100644
index 0000000000000..49799e4e67b09
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap
@@ -0,0 +1,142 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CalendarForm Renders calendar form 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Save
+
+
+
+
+ Cancel
+
+
+
+
+`;
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/calendar_form/calendar_form.js b/x-pack/plugins/ml/public/settings/calendars/edit/calendar_form/calendar_form.js
new file mode 100644
index 0000000000000..1f5aeab610610
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/calendar_form/calendar_form.js
@@ -0,0 +1,191 @@
+/*
+ * 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, { Fragment } from 'react';
+import { PropTypes } from 'prop-types';
+
+import {
+ EuiButton,
+ EuiComboBox,
+ EuiFieldText,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiForm,
+ EuiFormRow,
+ EuiSpacer,
+ EuiText,
+ EuiTitle,
+} from '@elastic/eui';
+
+import chrome from 'ui/chrome';
+import { EventsTable } from '../events_table/';
+
+
+function EditHeader({
+ calendarId,
+ description
+}) {
+ return (
+
+
+ Calendar {calendarId}
+
+
+
+ {description}
+
+
+
+
+ );
+}
+
+export function CalendarForm({
+ calendarId,
+ description,
+ eventsList,
+ groupIds,
+ isEdit,
+ isNewCalendarIdValid,
+ jobIds,
+ onCalendarIdChange,
+ onCreate,
+ onCreateGroupOption,
+ onDescriptionChange,
+ onEdit,
+ onEventDelete,
+ onGroupSelection,
+ showImportModal,
+ onJobSelection,
+ saving,
+ selectedGroupOptions,
+ selectedJobOptions,
+ showNewEventModal
+}) {
+ const msg = `Use lowercase alphanumerics (a-z and 0-9), hyphens or underscores;
+ must start and end with an alphanumeric character`;
+ const helpText = (isNewCalendarIdValid === true && !isEdit) ? msg : undefined;
+ const error = (isNewCalendarIdValid === false && !isEdit) ? [msg] : undefined;
+
+ return (
+
+ {!isEdit &&
+
+
+
+
+
+
+
+
+
+ }
+ {isEdit &&
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {saving ? 'Saving...' : 'Save'}
+
+
+
+
+ Cancel
+
+
+
+
+ );
+}
+
+CalendarForm.propTypes = {
+ calendarId: PropTypes.string.isRequired,
+ description: PropTypes.string.isRequired,
+ groupIds: PropTypes.array.isRequired,
+ isEdit: PropTypes.bool.isRequired,
+ isNewCalendarIdValid: PropTypes.bool.isRequired,
+ jobIds: PropTypes.array.isRequired,
+ onCalendarIdChange: PropTypes.func.isRequired,
+ onCreate: PropTypes.func.isRequired,
+ onCreateGroupOption: PropTypes.func.isRequired,
+ onDescriptionChange: PropTypes.func.isRequired,
+ onEdit: PropTypes.func.isRequired,
+ onEventDelete: PropTypes.func.isRequired,
+ onGroupSelection: PropTypes.func.isRequired,
+ showImportModal: PropTypes.func.isRequired,
+ onJobSelection: PropTypes.func.isRequired,
+ saving: PropTypes.bool.isRequired,
+ selectedGroupOptions: PropTypes.array.isRequired,
+ selectedJobOptions: PropTypes.array.isRequired,
+ showNewEventModal: PropTypes.func.isRequired,
+};
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/calendar_form/calendar_form.test.js b/x-pack/plugins/ml/public/settings/calendars/edit/calendar_form/calendar_form.test.js
new file mode 100644
index 0000000000000..004426006a38c
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/calendar_form/calendar_form.test.js
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+
+
+
+jest.mock('ui/chrome', () => ({
+ getBasePath: jest.fn()
+}));
+
+
+import { shallow, mount } from 'enzyme';
+import React from 'react';
+import { CalendarForm } from './calendar_form';
+
+const testProps = {
+ calendarId: '',
+ description: '',
+ eventsList: [],
+ groupIds: [],
+ isEdit: false,
+ isNewCalendarIdValid: false,
+ jobIds: [],
+ onCalendarIdChange: jest.fn(),
+ onCreate: jest.fn(),
+ onCreateGroupOption: jest.fn(),
+ onDescriptionChange: jest.fn(),
+ onEdit: jest.fn(),
+ onEventDelete: jest.fn(),
+ onGroupSelection: jest.fn(),
+ showImportModal: jest.fn(),
+ onJobSelection: jest.fn(),
+ saving: false,
+ selectedGroupOptions: [],
+ selectedJobOptions: [],
+ showNewEventModal: jest.fn()
+};
+
+describe('CalendarForm', () => {
+
+ test('Renders calendar form', () => {
+ const wrapper = shallow(
+
+ );
+
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('CalendarId shown as title when editing', () => {
+ const editProps = {
+ ...testProps,
+ isEdit: true,
+ calendarId: 'test-calendar',
+ description: 'test description',
+ };
+ const wrapper = mount(
+
+ );
+ const calendarId = wrapper.find('EuiTitle');
+
+ expect(
+ calendarId.containsMatchingElement(
+
Calendar test-calendar
+ )
+ ).toBeTruthy();
+ });
+
+});
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/calendar_form/index.js b/x-pack/plugins/ml/public/settings/calendars/edit/calendar_form/index.js
new file mode 100644
index 0000000000000..a0588647f2697
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/calendar_form/index.js
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+
+export { CalendarForm } from './calendar_form';
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/directive.js b/x-pack/plugins/ml/public/settings/calendars/edit/directive.js
new file mode 100644
index 0000000000000..9889ec2660c30
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/directive.js
@@ -0,0 +1,67 @@
+/*
+ * 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 React from 'react';
+import ReactDOM from 'react-dom';
+
+import { uiModules } from 'ui/modules';
+const module = uiModules.get('apps/ml', ['react']);
+
+import { checkFullLicense } from '../../../license/check_license';
+import { checkGetJobsPrivilege } from '../../../privilege/check_privilege';
+import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes';
+import { initPromise } from 'plugins/ml/util/promise';
+
+import uiRoutes from 'ui/routes';
+
+const template = `
+
+
+
+
+`;
+
+uiRoutes
+ .when('/settings/calendars_list/new_calendar', {
+ template,
+ resolve: {
+ CheckLicense: checkFullLicense,
+ privileges: checkGetJobsPrivilege,
+ checkMlNodesAvailable,
+ initPromise: initPromise(false)
+ }
+ })
+ .when('/settings/calendars_list/edit_calendar/:calendarId', {
+ template,
+ resolve: {
+ CheckLicense: checkFullLicense,
+ privileges: checkGetJobsPrivilege,
+ checkMlNodesAvailable,
+ initPromise: initPromise(false)
+ }
+ });
+
+import { NewCalendar } from './new_calendar.js';
+
+module.directive('mlNewCalendar', function ($route) {
+ return {
+ restrict: 'E',
+ replace: false,
+ scope: {},
+ link: function (scope, element) {
+ const props = {
+ calendarId: $route.current.params.calendarId
+ };
+
+ ReactDOM.render(
+ React.createElement(NewCalendar, props),
+ element[0]
+ );
+ }
+ };
+});
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap b/x-pack/plugins/ml/public/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap
new file mode 100644
index 0000000000000..0f632d6aa47bf
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap
@@ -0,0 +1,169 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EventsTable Renders events table with no search bar 1`] = `
+
+
+
+
+`;
+
+exports[`EventsTable Renders events table with search bar 1`] = `
+
+
+
+ New event
+ ,
+
+ Import events
+ ,
+ ],
+ }
+ }
+ sorting={
+ Object {
+ "sort": Object {
+ "direction": "asc",
+ "field": "description",
+ },
+ }
+ }
+ />
+
+`;
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/events_table/events_table.js b/x-pack/plugins/ml/public/settings/calendars/edit/events_table/events_table.js
new file mode 100644
index 0000000000000..eac9eed7bd984
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/events_table/events_table.js
@@ -0,0 +1,145 @@
+/*
+ * 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 moment from 'moment';
+
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiInMemoryTable,
+ EuiSpacer,
+} from '@elastic/eui';
+
+export const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
+
+function DeleteButton({ onClick }) {
+ return (
+
+
+ Delete
+
+
+ );
+}
+
+export function EventsTable({
+ eventsList,
+ onDeleteClick,
+ showSearchBar,
+ showImportModal,
+ showNewEventModal
+}) {
+ const sorting = {
+ sort: {
+ field: 'description',
+ direction: 'asc',
+ }
+ };
+
+ const pagination = {
+ initialPageSize: 5,
+ pageSizeOptions: [5, 10]
+ };
+
+ const columns = [
+ {
+ field: 'description',
+ name: 'Description',
+ sortable: true,
+ truncateText: true
+ },
+ {
+ field: 'start_time',
+ name: 'Start',
+ sortable: true,
+ render: (timeMs) => {
+ const time = moment(timeMs);
+ return time.format(TIME_FORMAT);
+ }
+ },
+ {
+ field: 'end_time',
+ name: 'End',
+ sortable: true,
+ render: (timeMs) => {
+ const time = moment(timeMs);
+ return time.format(TIME_FORMAT);
+ }
+ },
+ {
+ field: '',
+ name: '',
+ render: (event) => (
+ { onDeleteClick(event.event_id); }}
+ />
+ )
+ },
+ ];
+
+ const search = {
+ toolsRight: [(
+
+ New event
+ ),
+ (
+
+ Import events
+
+ )],
+ box: {
+ incremental: true,
+ },
+ filters: []
+ };
+
+ return (
+
+
+
+
+ );
+}
+
+EventsTable.propTypes = {
+ eventsList: PropTypes.array.isRequired,
+ onDeleteClick: PropTypes.func.isRequired,
+ showImportModal: PropTypes.func,
+ showNewEventModal: PropTypes.func,
+ showSearchBar: PropTypes.bool,
+};
+
+EventsTable.defaultProps = {
+ showSearchBar: false,
+};
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/events_table/events_table.test.js b/x-pack/plugins/ml/public/settings/calendars/edit/events_table/events_table.test.js
new file mode 100644
index 0000000000000..bcc26d12ec26f
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/events_table/events_table.test.js
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+
+
+
+jest.mock('ui/chrome', () => ({
+ getBasePath: jest.fn()
+}));
+
+
+import { shallow } from 'enzyme';
+import React from 'react';
+import { EventsTable } from './events_table';
+
+const testProps = {
+ eventsList: [{
+ calendar_id: 'test-calendar',
+ description: 'test description',
+ start_time: 1486656600000,
+ end_time: 1486657800000,
+ event_id: 'test-event-one'
+ }],
+ onDeleteClick: jest.fn(),
+ showSearchBar: false,
+ showImportModal: jest.fn(),
+ showNewEventModal: jest.fn()
+};
+
+describe('EventsTable', () => {
+
+ test('Renders events table with no search bar', () => {
+ const wrapper = shallow(
+
+ );
+
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('Renders events table with search bar', () => {
+ const showSearchBarProps = {
+ ...testProps,
+ showSearchBar: true,
+ };
+
+ const wrapper = shallow(
+
+ );
+
+ expect(wrapper).toMatchSnapshot();
+ });
+
+});
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/events_table/index.js b/x-pack/plugins/ml/public/settings/calendars/edit/events_table/index.js
new file mode 100644
index 0000000000000..daa2488e7aae3
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/events_table/index.js
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+
+export { EventsTable, TIME_FORMAT } from './events_table';
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/import_modal/__snapshots__/import_modal.test.js.snap b/x-pack/plugins/ml/public/settings/calendars/edit/import_modal/__snapshots__/import_modal.test.js.snap
new file mode 100644
index 0000000000000..d7c24fedc5318
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/import_modal/__snapshots__/import_modal.test.js.snap
@@ -0,0 +1,82 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ImportModal Renders import modal 1`] = `
+
+
+
+
+
+
+ Import events
+
+
+
+
+ Import events from an ICS file.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Import
+
+
+ Cancel
+
+
+
+
+`;
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/import_modal/import_modal.js b/x-pack/plugins/ml/public/settings/calendars/edit/import_modal/import_modal.js
new file mode 100644
index 0000000000000..bdadd72d3daf4
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/import_modal/import_modal.js
@@ -0,0 +1,202 @@
+/*
+ * 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, {
+ Component,
+ Fragment
+} from 'react';
+import { PropTypes } from 'prop-types';
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiCallOut,
+ EuiFilePicker,
+ EuiModal,
+ EuiModalHeader,
+ EuiModalHeaderTitle,
+ EuiModalBody,
+ EuiModalFooter,
+ EuiFlexGroup,
+ EuiFlexItem,
+} from '@elastic/eui';
+
+import { ImportedEvents } from '../imported_events';
+import { readFile, parseICSFile, filterEvents } from './utils';
+
+const MAX_FILE_SIZE_MB = 100;
+
+export class ImportModal extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ includePastEvents: false,
+ allImportedEvents: [],
+ selectedEvents: [],
+ fileLoading: false,
+ fileLoaded: false,
+ errorMessage: null,
+ };
+ }
+
+ handleImport = async (loadedFile) => {
+ const incomingFile = loadedFile[0];
+ const errorMessage = 'Could not parse ICS file.';
+ let events = [];
+
+ if (incomingFile && incomingFile.size <= (MAX_FILE_SIZE_MB * 1000000)) {
+ this.setState({ fileLoading: true, fileLoaded: true });
+
+ try {
+ const parsedFile = await readFile(incomingFile);
+ events = parseICSFile(parsedFile.data);
+
+ this.setState({
+ allImportedEvents: events,
+ selectedEvents: filterEvents(events),
+ fileLoading: false,
+ errorMessage: null,
+ includePastEvents: false
+ });
+ } catch (error) {
+ console.log(errorMessage, error);
+ this.setState({ errorMessage, fileLoading: false });
+ }
+ } else if (incomingFile && incomingFile.size > (MAX_FILE_SIZE_MB * 1000000)) {
+ this.setState({ fileLoading: false, errorMessage });
+ } else {
+ this.setState({ fileLoading: false, errorMessage: null });
+ }
+ }
+
+ onEventDelete = (eventId) => {
+ this.setState(prevState => ({
+ allImportedEvents: prevState.allImportedEvents.filter(event => event.event_id !== eventId),
+ selectedEvents: prevState.selectedEvents.filter(event => event.event_id !== eventId),
+ }));
+ }
+
+ onCheckboxToggle = (e) => {
+ this.setState({
+ includePastEvents: e.target.checked,
+ });
+ };
+
+ handleEventsAdd = () => {
+ const { allImportedEvents, selectedEvents, includePastEvents } = this.state;
+ const eventsToImport = includePastEvents ? allImportedEvents : selectedEvents;
+
+ const events = eventsToImport.map((event) => ({
+ description: event.description,
+ start_time: event.start_time,
+ end_time: event.end_time,
+ event_id: event.event_id
+ }));
+
+ this.props.addImportedEvents(events);
+ }
+
+ renderCallout = () => (
+
+ {this.state.errorMessage}
+
+ );
+
+ render() {
+ const { closeImportModal } = this.props;
+ const {
+ fileLoading,
+ fileLoaded,
+ allImportedEvents,
+ selectedEvents,
+ errorMessage,
+ includePastEvents
+ } = this.state;
+
+ let showRecurringWarning = false;
+ let importedEvents;
+
+ if (includePastEvents) {
+ importedEvents = allImportedEvents;
+ } else {
+ importedEvents = selectedEvents;
+ }
+
+ if (importedEvents.find(e => e.asterisk) !== undefined) {
+ showRecurringWarning = true;
+ }
+
+ return (
+
+
+
+
+
+
+ Import events
+
+
+
+ Import events from an ICS file.
+
+
+
+
+
+
+
+
+
+ {errorMessage !== null && this.renderCallout()}
+ {
+ allImportedEvents.length > 0 &&
+
+ }
+
+
+
+
+
+ Import
+
+
+ Cancel
+
+
+
+
+ );
+ }
+}
+
+ImportModal.propTypes = {
+ addImportedEvents: PropTypes.func.isRequired,
+ closeImportModal: PropTypes.func.isRequired,
+};
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/import_modal/import_modal.test.js b/x-pack/plugins/ml/public/settings/calendars/edit/import_modal/import_modal.test.js
new file mode 100644
index 0000000000000..271f67abf9c62
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/import_modal/import_modal.test.js
@@ -0,0 +1,65 @@
+/*
+ * 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, mount } from 'enzyme';
+import React from 'react';
+import { ImportModal } from './import_modal';
+
+const testProps = {
+ addImportedEvents: jest.fn(),
+ closeImportModal: jest.fn()
+};
+
+const events = [{
+ 'description': 'Downtime feb 9 2017 10:10 to 10:30',
+ 'start_time': 1486656600000,
+ 'end_time': 1486657800000,
+ 'calendar_id': 'farequote-calendar',
+ 'event_id': 'Ee-YgGcBxHgQWEhCO_xj'
+},
+{
+ 'description': 'New event!',
+ 'start_time': 1544076000000,
+ 'end_time': 1544162400000,
+ 'calendar_id': 'this-is-a-new-calendar',
+ 'event_id': 'ehWKhGcBqHkXuWNrIrSV'
+}];
+
+describe('ImportModal', () => {
+
+ test('Renders import modal', () => {
+ const wrapper = shallow(
+
+ );
+
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('Deletes selected event from event table', () => {
+ const wrapper = mount(
+
+ );
+
+ const testState = {
+ allImportedEvents: events,
+ selectedEvents: events,
+ };
+
+ const instance = wrapper.instance();
+
+ instance.setState(testState);
+ wrapper.update();
+ expect(wrapper.state('selectedEvents').length).toBe(2);
+ const deleteButton = wrapper.find('[data-testid="event_delete"]');
+ const button = deleteButton.find('EuiButtonEmpty').first();
+ button.simulate('click');
+
+ expect(wrapper.state('selectedEvents').length).toBe(1);
+ });
+
+});
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/import_modal/index.js b/x-pack/plugins/ml/public/settings/calendars/edit/import_modal/index.js
new file mode 100644
index 0000000000000..1dd604712b99f
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/import_modal/index.js
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+
+export { ImportModal } from './import_modal';
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/import_modal/utils.js b/x-pack/plugins/ml/public/settings/calendars/edit/import_modal/utils.js
new file mode 100644
index 0000000000000..5d67383e53c8f
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/import_modal/utils.js
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+
+
+
+const icalendar = require('icalendar');
+import moment from 'moment';
+import { generateTempId } from '../utils';
+
+
+function createEvents(ical) {
+ const events = ical.events();
+ const mlEvents = [];
+
+ events.forEach((e) => {
+ if (e.element === 'VEVENT') {
+ const description = e.properties.SUMMARY;
+ const start = e.properties.DTSTART;
+ const end = e.properties.DTEND;
+ const recurring = (e.properties.RRULE !== undefined);
+
+ if (description && start && end && description.length && start.length && end.length) {
+ // Temp reference to unsaved events to allow removal from table
+ const tempId = generateTempId();
+
+ mlEvents.push({
+ event_id: tempId,
+ description: description[0].value,
+ start_time: start[0].value.valueOf(),
+ end_time: end[0].value.valueOf(),
+ asterisk: recurring
+ });
+ }
+ }
+ });
+ return mlEvents;
+}
+
+export function filterEvents(events) {
+ const now = moment().valueOf();
+ return events.filter(e => e.start_time > now);
+}
+
+export function parseICSFile(data) {
+ const cal = icalendar.parse_calendar(data);
+ return createEvents(cal);
+}
+
+export function readFile(file) {
+ return new Promise((resolve, reject) => {
+ if (file && file.size) {
+ const reader = new FileReader();
+ reader.readAsText(file);
+
+ reader.onload = (() => {
+ return () => {
+ const data = reader.result;
+ if (data === '') {
+ reject();
+ } else {
+ resolve({ data });
+ }
+ };
+ })(file);
+ } else {
+ reject();
+ }
+ });
+}
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/imported_events/__snapshots__/imported_events.test.js.snap b/x-pack/plugins/ml/public/settings/calendars/edit/imported_events/__snapshots__/imported_events.test.js.snap
new file mode 100644
index 0000000000000..17b7a10d5b924
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/imported_events/__snapshots__/imported_events.test.js.snap
@@ -0,0 +1,60 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ImportedEvents Renders imported events 1`] = `
+
+
+
+
+
+ Events to import:
+ 1
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/imported_events/imported_events.js b/x-pack/plugins/ml/public/settings/calendars/edit/imported_events/imported_events.js
new file mode 100644
index 0000000000000..488d1541c96cf
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/imported_events/imported_events.js
@@ -0,0 +1,63 @@
+/*
+ * 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, { Fragment } from 'react';
+import { PropTypes } from 'prop-types';
+import {
+ EuiCheckbox,
+ EuiFlexItem,
+ EuiText,
+ EuiSpacer
+} from '@elastic/eui';
+import { EventsTable } from '../events_table/';
+
+
+export function ImportedEvents({
+ events,
+ showRecurringWarning,
+ includePastEvents,
+ onCheckboxToggle,
+ onEventDelete,
+}) {
+ return (
+
+
+
+
+ Events to import: {events.length}
+ {showRecurringWarning && (
+
+ Recurring events not supported. Only the first event will be imported.
+ )
+ }
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+ImportedEvents.propTypes = {
+ events: PropTypes.array.isRequired,
+ showRecurringWarning: PropTypes.bool.isRequired,
+ includePastEvents: PropTypes.bool.isRequired,
+ onCheckboxToggle: PropTypes.func.isRequired,
+ onEventDelete: PropTypes.func.isRequired,
+};
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/imported_events/imported_events.test.js b/x-pack/plugins/ml/public/settings/calendars/edit/imported_events/imported_events.test.js
new file mode 100644
index 0000000000000..36451bba5ceb1
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/imported_events/imported_events.test.js
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+
+
+jest.mock('ui/chrome', () => ({
+ getBasePath: jest.fn()
+}));
+
+
+import { shallow } from 'enzyme';
+import React from 'react';
+import { ImportedEvents } from './imported_events';
+
+const testProps = {
+ events: [{
+ calendar_id: 'test-calendar',
+ description: 'test description',
+ start_time: 1486656600000,
+ end_time: 1486657800000,
+ event_id: 'test-event-one'
+ }],
+ showRecurringWarning: false,
+ includePastEvents: false,
+ onCheckboxToggle: jest.fn(),
+ onEventDelete: jest.fn(),
+};
+
+describe('ImportedEvents', () => {
+
+ test('Renders imported events', () => {
+ const wrapper = shallow(
+
+ );
+
+ expect(wrapper).toMatchSnapshot();
+ });
+
+});
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/imported_events/index.js b/x-pack/plugins/ml/public/settings/calendars/edit/imported_events/index.js
new file mode 100644
index 0000000000000..e52b38005a530
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/imported_events/index.js
@@ -0,0 +1,7 @@
+/*
+ * 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.
+ */
+
+export { ImportedEvents } from './imported_events';
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/index.js b/x-pack/plugins/ml/public/settings/calendars/edit/index.js
new file mode 100644
index 0000000000000..fd75a9ceb9b49
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/index.js
@@ -0,0 +1,9 @@
+/*
+ * 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 './directive';
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/new_calendar.js b/x-pack/plugins/ml/public/settings/calendars/edit/new_calendar.js
new file mode 100644
index 0000000000000..f7246c566878a
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/new_calendar.js
@@ -0,0 +1,317 @@
+/*
+ * 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, {
+ Component
+} from 'react';
+import { PropTypes } from 'prop-types';
+
+import {
+ EuiPage,
+ EuiPageContent,
+ EuiOverlayMask,
+} from '@elastic/eui';
+
+import chrome from 'ui/chrome';
+import { getCalendarSettingsData, validateCalendarId } from './utils';
+import { CalendarForm } from './calendar_form/';
+import { NewEventModal } from './new_event_modal/';
+import { ImportModal } from './import_modal';
+import { ml } from '../../../services/ml_api_service';
+import { toastNotifications } from 'ui/notify';
+
+export class NewCalendar extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isNewEventModalVisible: false,
+ isImportModalVisible: false,
+ isNewCalendarIdValid: null,
+ loading: true,
+ jobIds: [],
+ jobIdOptions: [],
+ groupIds: [],
+ groupIdOptions: [],
+ calendars: [],
+ formCalendarId: '',
+ description: '',
+ selectedJobOptions: [],
+ selectedGroupOptions: [],
+ events: [],
+ saving: false,
+ selectedCalendar: undefined,
+ };
+ }
+
+ componentDidMount() {
+ this.formSetup();
+ }
+
+ async formSetup() {
+ try {
+ const { jobIds, groupIds, calendars } = await getCalendarSettingsData();
+
+ const jobIdOptions = jobIds.map((jobId) => ({ label: jobId }));
+ const groupIdOptions = groupIds.map((groupId) => ({ label: groupId }));
+
+ const selectedJobOptions = [];
+ const selectedGroupOptions = [];
+ let eventsList = [];
+ let selectedCalendar;
+ let formCalendarId = '';
+
+ // Editing existing calendar.
+ if (this.props.calendarId !== undefined) {
+ selectedCalendar = calendars.find((cal) => cal.calendar_id === this.props.calendarId);
+
+ if (selectedCalendar) {
+ formCalendarId = selectedCalendar.calendar_id;
+ eventsList = selectedCalendar.events;
+
+ selectedCalendar.job_ids.forEach(id => {
+ if (jobIds.find((jobId) => jobId === id)) {
+ selectedJobOptions.push({ label: id });
+ } else if (groupIds.find((groupId) => groupId === id)) {
+ selectedGroupOptions.push({ label: id });
+ }
+ });
+ }
+ }
+
+ this.setState({
+ events: eventsList,
+ formCalendarId,
+ jobIds,
+ jobIdOptions,
+ groupIds,
+ groupIdOptions,
+ calendars,
+ loading: false,
+ selectedJobOptions,
+ selectedGroupOptions,
+ selectedCalendar
+ });
+ } catch (error) {
+ console.log(error);
+ this.setState({ loading: false });
+ toastNotifications.addDanger('An error occurred loading calendar form data. Try refreshing the page.');
+ }
+ }
+
+ onCreate = async () => {
+ const calendar = this.setUpCalendarForApi();
+ this.setState({ saving: true });
+
+ try {
+ await ml.addCalendar(calendar);
+ window.location = `${chrome.getBasePath()}/app/ml#/settings/calendars_list`;
+ } catch (error) {
+ console.log('Error saving calendar', error);
+ this.setState({ saving: false });
+ toastNotifications.addDanger(`An error occurred creating calendar ${calendar.calendarId}`);
+ }
+ }
+
+ onEdit = async () => {
+ const calendar = this.setUpCalendarForApi();
+ this.setState({ saving: true });
+
+ try {
+ await ml.updateCalendar(calendar);
+ window.location = `${chrome.getBasePath()}/app/ml#/settings/calendars_list`;
+ } catch (error) {
+ console.log('Error saving calendar', error);
+ this.setState({ saving: false });
+ toastNotifications.addDanger(`An error occurred saving calendar ${calendar.calendarId}. Try refreshing the page.`);
+ }
+ }
+
+ setUpCalendarForApi = () => {
+ const {
+ formCalendarId,
+ description,
+ events,
+ selectedGroupOptions,
+ selectedJobOptions,
+ } = this.state;
+
+ const jobIds = selectedJobOptions.map((option) => option.label);
+ const groupIds = selectedGroupOptions.map((option) => option.label);
+
+ // Reduce events to fields expected by api
+ const eventsToSave = events.map((event) => ({
+ description: event.description,
+ start_time: event.start_time,
+ end_time: event.end_time
+ }));
+
+ // set up calendar
+ const calendar = {
+ calendarId: formCalendarId,
+ description,
+ events: eventsToSave,
+ job_ids: [...jobIds, ...groupIds]
+ };
+
+ return calendar;
+ }
+
+ onCreateGroupOption = (newGroup) => {
+ const newOption = {
+ label: newGroup,
+ };
+ // Select the option.
+ this.setState(prevState => ({
+ selectedGroupOptions: prevState.selectedGroupOptions.concat(newOption),
+ }));
+ };
+
+ onJobSelection = (selectedJobOptions) => {
+ this.setState({
+ selectedJobOptions,
+ });
+ };
+
+ onGroupSelection = (selectedGroupOptions) => {
+ this.setState({
+ selectedGroupOptions,
+ });
+ };
+
+ onCalendarIdChange = (e) => {
+ const isValid = validateCalendarId(e.target.value);
+
+ this.setState({
+ formCalendarId: e.target.value,
+ isNewCalendarIdValid: isValid
+ });
+ };
+
+ onDescriptionChange = (e) => {
+ this.setState({
+ description: e.target.value,
+ });
+ };
+
+ showImportModal = () => {
+ this.setState(prevState => ({
+ isImportModalVisible: !prevState.isImportModalVisible,
+ }));
+ }
+
+ closeImportModal = () => {
+ this.setState({
+ isImportModalVisible: false,
+ });
+ }
+
+ onEventDelete = (eventId) => {
+ this.setState(prevState => ({
+ events: prevState.events.filter(event => event.event_id !== eventId)
+ }));
+ }
+
+ closeNewEventModal = () => {
+ this.setState({ isNewEventModalVisible: false });
+ }
+
+ showNewEventModal = () => {
+ this.setState({ isNewEventModalVisible: true });
+ }
+
+ addEvent = (event) => {
+ this.setState(prevState => ({
+ events: [...prevState.events, event],
+ isNewEventModalVisible: false
+ }));
+ }
+
+ addImportedEvents = (events) => {
+ this.setState(prevState => ({
+ events: [...prevState.events, ...events],
+ isImportModalVisible: false
+ }));
+ }
+
+ render() {
+ const {
+ events,
+ isNewEventModalVisible,
+ isImportModalVisible,
+ isNewCalendarIdValid,
+ formCalendarId,
+ description,
+ groupIdOptions,
+ jobIdOptions,
+ saving,
+ selectedCalendar,
+ selectedJobOptions,
+ selectedGroupOptions
+ } = this.state;
+
+ let modal = '';
+
+ if (isNewEventModalVisible) {
+ modal = (
+
+
+
+ );
+ } else if (isImportModalVisible) {
+ modal = (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ {modal}
+
+ );
+ }
+}
+
+NewCalendar.propTypes = {
+ calendarId: PropTypes.string,
+};
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/new_calendar.test.js b/x-pack/plugins/ml/public/settings/calendars/edit/new_calendar.test.js
new file mode 100644
index 0000000000000..a65d552ddd019
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/new_calendar.test.js
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ */
+
+
+
+jest.mock('../../../privilege/check_privilege', () => ({
+ checkPermission: () => true
+}));
+jest.mock('../../../license/check_license', () => ({
+ hasLicenseExpired: () => false
+}));
+jest.mock('../../../privilege/get_privileges', () => ({
+ getPrivileges: () => {}
+}));
+jest.mock('../../../ml_nodes_check/check_ml_nodes', () => ({
+ mlNodesAvailable: () => true
+}));
+jest.mock('ui/chrome', () => ({
+ getBasePath: jest.fn()
+}));
+jest.mock('../../../services/ml_api_service', () => ({
+ ml: {
+ calendars: () => {
+ return Promise.resolve([]);
+ },
+ jobs: {
+ jobsSummary: () => {
+ return Promise.resolve([]);
+ },
+ groups: () => {
+ return Promise.resolve([]);
+ },
+ },
+ }
+}));
+jest.mock('./utils', () => ({
+ getCalendarSettingsData: jest.fn().mockImplementation(() => new Promise((resolve) => {
+ resolve({
+ jobIds: ['test-job-one', 'test-job-2'],
+ groupIds: ['test-group-one', 'test-group-two'],
+ calendars: []
+ });
+ })),
+}));
+
+import { shallow, mount } from 'enzyme';
+import React from 'react';
+import { NewCalendar } from './new_calendar';
+
+describe('NewCalendar', () => {
+
+ test('Renders new calendar form', () => {
+ const wrapper = shallow(
+
+ );
+
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('Import modal shown on Import Events button click', () => {
+ const wrapper = mount(
+
+ );
+
+ const importButton = wrapper.find('[data-testid="ml_import_events"]');
+ const button = importButton.find('EuiButton');
+ button.simulate('click');
+
+ expect(wrapper.state('isImportModalVisible')).toBe(true);
+ });
+
+ test('New event modal shown on New event button click', () => {
+ const wrapper = mount(
+
+ );
+
+ const importButton = wrapper.find('[data-testid="ml_new_event"]');
+ const button = importButton.find('EuiButton');
+ button.simulate('click');
+
+ expect(wrapper.state('isNewEventModalVisible')).toBe(true);
+ });
+
+});
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/new_event_modal/index.js b/x-pack/plugins/ml/public/settings/calendars/edit/new_event_modal/index.js
new file mode 100644
index 0000000000000..3c6d2e34d61e7
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/new_event_modal/index.js
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+export { NewEventModal } from './new_event_modal';
+
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/new_event_modal/new_event_modal.js b/x-pack/plugins/ml/public/settings/calendars/edit/new_event_modal/new_event_modal.js
new file mode 100644
index 0000000000000..858b60757790e
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/new_event_modal/new_event_modal.js
@@ -0,0 +1,289 @@
+/*
+ * 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, {
+ Component,
+ Fragment
+} from 'react';
+import { PropTypes } from 'prop-types';
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiDatePicker,
+ EuiDatePickerRange,
+ EuiFieldText,
+ EuiForm,
+ EuiFormRow,
+ EuiModal,
+ EuiModalHeader,
+ EuiModalHeaderTitle,
+ EuiModalBody,
+ EuiModalFooter,
+ EuiSpacer,
+ EuiFlexGroup,
+ EuiFlexItem,
+} from '@elastic/eui';
+import moment from 'moment';
+import { TIME_FORMAT } from '../events_table/';
+import { generateTempId } from '../utils';
+
+const VALID_DATE_STRING_LENGTH = 19;
+
+export class NewEventModal extends Component {
+ constructor(props) {
+ super(props);
+
+ const startDate = moment().startOf('day');
+ const endDate = moment().startOf('day').add(1, 'days');
+
+ this.state = {
+ startDate,
+ endDate,
+ description: '',
+ startDateString: startDate.format(TIME_FORMAT),
+ endDateString: endDate.format(TIME_FORMAT)
+ };
+ }
+
+ onDescriptionChange = (e) => {
+ this.setState({
+ description: e.target.value,
+ });
+ };
+
+ handleAddEvent = () => {
+ const { description, startDate, endDate } = this.state;
+ // Temp reference to unsaved events to allow removal from table
+ const tempId = generateTempId();
+
+ const event = {
+ description,
+ start_time: startDate.valueOf(),
+ end_time: endDate.valueOf(),
+ event_id: tempId
+ };
+
+ this.props.addEvent(event);
+ }
+
+ handleChangeStart = (date) => {
+ let start = null;
+ let end = this.state.endDate;
+
+ const startMoment = moment(date);
+ const endMoment = moment(date);
+
+ start = startMoment.startOf('day');
+
+ if (start > end) {
+ end = endMoment.startOf('day').add(1, 'days');
+ }
+ this.setState({
+ startDate: start,
+ endDate: end,
+ startDateString: start.format(TIME_FORMAT),
+ endDateString: end.format(TIME_FORMAT)
+ });
+ }
+
+ handleChangeEnd = (date) => {
+ let start = this.state.startDate;
+ let end = null;
+
+ const startMoment = moment(date);
+ const endMoment = moment(date);
+
+ end = endMoment.startOf('day');
+
+ if (start > end) {
+ start = startMoment.startOf('day').subtract(1, 'days');
+ }
+ this.setState({
+ startDate: start,
+ endDate: end,
+ startDateString: start.format(TIME_FORMAT),
+ endDateString: end.format(TIME_FORMAT)
+ });
+ }
+
+ handleTimeStartChange = (event) => {
+ const dateString = event.target.value;
+ let isValidDate = false;
+
+ if (dateString.length === VALID_DATE_STRING_LENGTH) {
+ isValidDate = moment(dateString).isValid(TIME_FORMAT, true);
+ } else {
+ this.setState({
+ startDateString: dateString,
+ });
+ }
+
+ if (isValidDate) {
+ this.setState({
+ startDateString: dateString,
+ startDate: moment(dateString)
+ });
+ }
+ }
+
+ handleTimeEndChange = (event) => {
+ const dateString = event.target.value;
+ let isValidDate = false;
+
+ if (dateString.length === VALID_DATE_STRING_LENGTH) {
+ isValidDate = moment(dateString).isValid(TIME_FORMAT, true);
+ } else {
+ this.setState({
+ endDateString: dateString,
+ });
+ }
+
+ if (isValidDate) {
+ this.setState({
+ endDateString: dateString,
+ endDate: moment(dateString)
+ });
+ }
+ }
+
+ renderRangedDatePicker = () => {
+ const {
+ startDate,
+ endDate,
+ startDateString,
+ endDateString,
+ } = this.state;
+
+ const timeInputs = (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+ return (
+
+
+ {timeInputs}
+
+
+ endDate}
+ aria-label="Start date"
+ timeFormat={TIME_FORMAT}
+ dateFormat={TIME_FORMAT}
+ />
+ }
+ endDateControl={
+ endDate}
+ aria-label="End date"
+ timeFormat={TIME_FORMAT}
+ dateFormat={TIME_FORMAT}
+ />
+ }
+ />
+
+
+ );
+ }
+
+ render() {
+ const { closeModal } = this.props;
+ const { description } = this.state;
+
+ return (
+
+
+
+
+ Create new event
+
+
+
+
+
+
+
+
+ {this.renderRangedDatePicker()}
+
+
+
+
+
+
+ Add
+
+
+ Cancel
+
+
+
+
+ );
+ }
+}
+
+NewEventModal.propTypes = {
+ closeModal: PropTypes.func.isRequired,
+ addEvent: PropTypes.func.isRequired,
+};
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/new_event_modal/new_event_modal.test.js b/x-pack/plugins/ml/public/settings/calendars/edit/new_event_modal/new_event_modal.test.js
new file mode 100644
index 0000000000000..00541866eb7ee
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/new_event_modal/new_event_modal.test.js
@@ -0,0 +1,76 @@
+/*
+ * 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 { NewEventModal } from './new_event_modal';
+import moment from 'moment';
+
+const testProps = {
+ closeModal: jest.fn(),
+ addEvent: jest.fn(),
+};
+
+const stateTimestamps = {
+ startDate: 1544508000000,
+ endDate: 1544594400000
+};
+
+describe('NewEventModal', () => {
+
+ it('Add button disabled if description empty', () => {
+ const wrapper = shallow(
+
+ );
+
+ const addButton = wrapper.find('EuiButton').first();
+ expect(addButton.prop('disabled')).toBe(true);
+ });
+
+ it('if endDate is less than startDate should set startDate one day before endDate', () => {
+ const wrapper = shallow();
+ const instance = wrapper.instance();
+ instance.setState({
+ startDate: moment(stateTimestamps.startDate),
+ endDate: moment(stateTimestamps.endDate)
+ });
+ // set to Dec 11, 2018 and Dec 12, 2018
+ const startMoment = moment(stateTimestamps.startDate);
+ const endMoment = moment(stateTimestamps.endDate);
+ // make startMoment greater than current end Date
+ startMoment.startOf('day').add(3, 'days');
+ // trigger handleChangeStart directly with startMoment
+ instance.handleChangeStart(startMoment);
+ // add 3 days to endMoment as it will be adjusted to be one day after startDate
+ const expected = endMoment.startOf('day').add(3, 'days').format();
+
+ expect(wrapper.state('endDate').format()).toBe(expected);
+ });
+
+ it('if startDate is greater than endDate should set endDate one day after startDate', () => {
+ const wrapper = shallow();
+ const instance = wrapper.instance();
+ instance.setState({
+ startDate: moment(stateTimestamps.startDate),
+ endDate: moment(stateTimestamps.endDate)
+ });
+
+ // set to Dec 11, 2018 and Dec 12, 2018
+ const startMoment = moment(stateTimestamps.startDate);
+ const endMoment = moment(stateTimestamps.endDate);
+ // make endMoment less than current start Date
+ endMoment.startOf('day').subtract(3, 'days');
+ // trigger handleChangeStart directly with endMoment
+ instance.handleChangeStart(endMoment);
+ // subtract 3 days from startDate as it will be adjusted to be one day before endDate
+ const expected = startMoment.startOf('day').subtract(2, 'days').format();
+
+ expect(wrapper.state('startDate').format()).toBe(expected);
+ });
+
+});
diff --git a/x-pack/plugins/ml/public/settings/calendars/edit/utils.js b/x-pack/plugins/ml/public/settings/calendars/edit/utils.js
new file mode 100644
index 0000000000000..1a0606578f1b5
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/edit/utils.js
@@ -0,0 +1,87 @@
+/*
+ * 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 } from '../../../services/ml_api_service';
+import { isJobIdValid } from '../../../../common/util/job_utils';
+
+
+function getJobIds() {
+ return new Promise((resolve, reject) => {
+ ml.jobs.jobsSummary()
+ .then((resp) => {
+ resolve(resp.map((job) => job.id));
+ })
+ .catch((err) => {
+ const errorMessage = `Error fetching job summaries: ${err}`;
+ console.log(errorMessage);
+ reject(errorMessage);
+ });
+ });
+}
+
+function getGroupIds() {
+ return new Promise((resolve, reject) => {
+ ml.jobs.groups()
+ .then((resp) => {
+ resolve(resp.map((group) => group.id));
+ })
+ .catch((err) => {
+ const errorMessage = `Error loading groups: ${err}`;
+ console.log(errorMessage);
+ reject(errorMessage);
+ });
+ });
+}
+
+function getCalendars() {
+ return new Promise((resolve, reject) => {
+ ml.calendars()
+ .then((resp) => {
+ resolve(resp);
+ })
+ .catch((err) => {
+ const errorMessage = `Error loading calendars: ${err}`;
+ console.log(errorMessage);
+ reject(errorMessage);
+ });
+ });
+}
+
+export function getCalendarSettingsData() {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const data = await Promise.all([getJobIds(), getGroupIds(), getCalendars()]);
+
+ const formattedData = {
+ jobIds: data[0],
+ groupIds: data[1],
+ calendars: data[2]
+ };
+ resolve(formattedData);
+ } catch (error) {
+ console.log(error);
+ reject(error);
+ }
+ });
+}
+
+export function validateCalendarId(calendarId) {
+ let valid = true;
+
+ if (calendarId === '' || calendarId === undefined) {
+ valid = false;
+ } else if (isJobIdValid(calendarId) === false) {
+ valid = false;
+ }
+
+ return valid;
+}
+
+export function generateTempId() {
+ return Math.random().toString(36).substr(2, 9);
+}
diff --git a/x-pack/plugins/ml/public/settings/calendars/index.js b/x-pack/plugins/ml/public/settings/calendars/index.js
new file mode 100644
index 0000000000000..bcc62f4c5b10e
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/index.js
@@ -0,0 +1,10 @@
+/*
+ * 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 './list';
+import './edit';
diff --git a/x-pack/plugins/ml/public/settings/calendars/list/__snapshots__/calendars_list.test.js.snap b/x-pack/plugins/ml/public/settings/calendars/list/__snapshots__/calendars_list.test.js.snap
new file mode 100644
index 0000000000000..2be04c34494ec
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/list/__snapshots__/calendars_list.test.js.snap
@@ -0,0 +1,65 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CalendarsList Renders calendar list with calendars 1`] = `
+
+
+
+
+
+`;
diff --git a/x-pack/plugins/ml/public/settings/calendars/list/_index.scss b/x-pack/plugins/ml/public/settings/calendars/list/_index.scss
new file mode 100644
index 0000000000000..7c77179197bc4
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/list/_index.scss
@@ -0,0 +1 @@
+@import 'list';
diff --git a/x-pack/plugins/ml/public/settings/calendars/list/_list.scss b/x-pack/plugins/ml/public/settings/calendars/list/_list.scss
new file mode 100644
index 0000000000000..4587859f6b33d
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/list/_list.scss
@@ -0,0 +1,9 @@
+.mlCalendarList {
+
+ .mlCalendarList__content {
+ max-width: map-get($euiBreakpoints, 'xl');
+ margin-top: $euiSize;
+ margin-bottom: $euiSize;
+ }
+
+}
diff --git a/x-pack/plugins/ml/public/settings/calendars/list/calendars_list.js b/x-pack/plugins/ml/public/settings/calendars/list/calendars_list.js
new file mode 100644
index 0000000000000..3ccddf7251d78
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/list/calendars_list.js
@@ -0,0 +1,149 @@
+/*
+ * 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, {
+ Component
+} from 'react';
+
+import {
+ EuiConfirmModal,
+ EuiOverlayMask,
+ EuiPage,
+ EuiPageContent,
+ EUI_MODAL_CONFIRM_BUTTON,
+} from '@elastic/eui';
+
+import { CalendarsListTable } from './table/';
+import { ml } from '../../../services/ml_api_service';
+import { toastNotifications } from 'ui/notify';
+import { checkPermission } from '../../../privilege/check_privilege';
+import { mlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes';
+import { deleteCalendars } from './delete_calendars';
+
+export class CalendarsList extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ loading: true,
+ calendars: [],
+ isDestroyModalVisible: false,
+ calendarId: null,
+ selectedForDeletion: [],
+ canCreateCalendar: checkPermission('canCreateCalendar'),
+ canDeleteCalendar: checkPermission('canDeleteCalendar'),
+ nodesAvailable: mlNodesAvailable()
+ };
+ }
+
+ loadCalendars = async () => {
+ try {
+ const calendars = await ml.calendars();
+
+ this.setState({
+ calendars,
+ loading: false,
+ isDestroyModalVisible: false,
+ });
+ } catch (error) {
+ console.log(error);
+ this.setState({ loading: false });
+ toastNotifications.addDanger('An error occurred loading the list of calendars.');
+ }
+ }
+
+ closeDestroyModal = () => {
+ this.setState({ isDestroyModalVisible: false, calendarId: null });
+ }
+
+ showDestroyModal = () => {
+ this.setState({ isDestroyModalVisible: true });
+ }
+
+ setSelectedCalendarList = (selectedCalendars) => {
+ this.setState({ selectedForDeletion: selectedCalendars });
+ }
+
+ deleteCalendars = () => {
+ const { selectedForDeletion } = this.state;
+
+ this.closeDestroyModal();
+ deleteCalendars(selectedForDeletion, this.loadCalendars);
+ }
+
+ addRequiredFieldsToList = (calendarsList = []) => {
+ for (let i = 0; i < calendarsList.length; i++) {
+ const eventLength = calendarsList[i].events.length;
+ calendarsList[i].job_ids_string = calendarsList[i].job_ids.join(', ');
+ calendarsList[i].events_length = `${eventLength} ${eventLength === 1 ? 'event' : 'events'}`;
+ }
+
+ return calendarsList;
+ }
+
+ componentDidMount() {
+ this.loadCalendars();
+ }
+
+ render() {
+ const {
+ calendars,
+ selectedForDeletion,
+ loading,
+ canCreateCalendar,
+ canDeleteCalendar,
+ nodesAvailable
+ } = this.state;
+ let destroyModal = '';
+
+ if (this.state.isDestroyModalVisible) {
+ destroyModal = (
+
+
+
+ {
+ `Delete ${selectedForDeletion.length === 1 ? 'this' : 'these'}
+ calendar${selectedForDeletion.length === 1 ? '' : 's'}?
+ ${selectedForDeletion.map((c) => c.calendar_id).join(', ')}`
+ }
+
+
+
+ );
+ }
+
+ return (
+
+
+ 0}
+ />
+
+ {destroyModal}
+
+ );
+ }
+}
diff --git a/x-pack/plugins/ml/public/settings/calendars/list/calendars_list.test.js b/x-pack/plugins/ml/public/settings/calendars/list/calendars_list.test.js
new file mode 100644
index 0000000000000..3d55c1e47c06c
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/list/calendars_list.test.js
@@ -0,0 +1,110 @@
+/*
+ * 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.
+ */
+
+
+
+jest.mock('../../../privilege/check_privilege', () => ({
+ checkPermission: () => true
+}));
+jest.mock('../../../license/check_license', () => ({
+ hasLicenseExpired: () => false
+}));
+jest.mock('../../../privilege/get_privileges', () => ({
+ getPrivileges: () => {}
+}));
+jest.mock('../../../ml_nodes_check/check_ml_nodes', () => ({
+ mlNodesAvailable: () => true
+}));
+jest.mock('ui/chrome', () => ({
+ getBasePath: jest.fn()
+}));
+jest.mock('../../../services/ml_api_service', () => ({
+ ml: {
+ calendars: () => {
+ return Promise.resolve([]);
+ },
+ delete: jest.fn(),
+ }
+}));
+
+import { shallow, mount } from 'enzyme';
+import React from 'react';
+import { ml } from '../../../services/ml_api_service';
+
+import { CalendarsList } from './calendars_list';
+
+const testingState = {
+ loading: false,
+ calendars: [
+ {
+ 'calendar_id': 'farequote-calendar',
+ 'job_ids': ['farequote'],
+ 'description': 'test ',
+ 'events': [{
+ 'description': 'Downtime feb 9 2017 10:10 to 10:30',
+ 'start_time': 1486656600000,
+ 'end_time': 1486657800000,
+ 'calendar_id': 'farequote-calendar',
+ 'event_id': 'Ee-YgGcBxHgQWEhCO_xj'
+ }]
+ },
+ {
+ 'calendar_id': 'this-is-a-new-calendar',
+ 'job_ids': ['test'],
+ 'description': 'new calendar',
+ 'events': [{
+ 'description': 'New event!',
+ 'start_time': 1544076000000,
+ 'end_time': 1544162400000,
+ 'calendar_id': 'this-is-a-new-calendar',
+ 'event_id': 'ehWKhGcBqHkXuWNrIrSV'
+ }]
+ }],
+ isDestroyModalVisible: false,
+ calendarId: null,
+ selectedForDeletion: [],
+ canCreateCalendar: true,
+ canDeleteCalendar: true,
+ nodesAvailable: true,
+};
+
+describe('CalendarsList', () => {
+
+ test('loads calendars on mount', () => {
+ ml.calendars = jest.fn();
+ shallow(
+
+ );
+
+ expect(ml.calendars).toHaveBeenCalled();
+ });
+
+ test('Renders calendar list with calendars', () => {
+ const wrapper = shallow(
+
+ );
+
+ wrapper.instance().setState(testingState);
+ wrapper.update();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('Sets selected calendars list on checkbox change', () => {
+ const wrapper = mount(
+
+ );
+
+ const instance = wrapper.instance();
+ const spy = jest.spyOn(instance, 'setSelectedCalendarList');
+ instance.setState(testingState);
+ wrapper.update();
+
+ const checkbox = wrapper.find('input[type="checkbox"]').first();
+ checkbox.simulate('change');
+ expect(spy).toHaveBeenCalled();
+ });
+
+});
diff --git a/x-pack/plugins/ml/public/settings/calendars/list/delete_calendars.js b/x-pack/plugins/ml/public/settings/calendars/list/delete_calendars.js
new file mode 100644
index 0000000000000..36dfe87a53bab
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/list/delete_calendars.js
@@ -0,0 +1,38 @@
+/*
+ * 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 { toastNotifications } from 'ui/notify';
+import { ml } from '../../../services/ml_api_service';
+
+
+export async function deleteCalendars(calendarsToDelete, callback) {
+ if (calendarsToDelete === undefined || calendarsToDelete.length === 0) {
+ return;
+ }
+
+ // Delete each of the specified calendars in turn, waiting for each response
+ // before deleting the next to minimize load on the cluster.
+ const messageId = `${(calendarsToDelete.length > 1) ?
+ `${calendarsToDelete.length} calendars` : calendarsToDelete[0].calendar_id}`;
+ toastNotifications.add(`Deleting ${messageId}`);
+
+ for(const calendar of calendarsToDelete) {
+ const calendarId = calendar.calendar_id;
+ try {
+ await ml.deleteCalendar({ calendarId });
+ } catch (error) {
+ console.log('Error deleting calendar:', error);
+ let errorMessage = `An error occurred deleting calendar ${calendar.calendar_id}`;
+ if (error.message) {
+ errorMessage += ` : ${error.message}`;
+ }
+ toastNotifications.addDanger(errorMessage);
+ }
+ }
+
+ toastNotifications.addSuccess(`${messageId} deleted`);
+ callback();
+}
diff --git a/x-pack/plugins/ml/public/settings/calendars/list/directive.js b/x-pack/plugins/ml/public/settings/calendars/list/directive.js
new file mode 100644
index 0000000000000..4ecfb85882a10
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/list/directive.js
@@ -0,0 +1,55 @@
+/*
+ * 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 React from 'react';
+import ReactDOM from 'react-dom';
+
+import { uiModules } from 'ui/modules';
+const module = uiModules.get('apps/ml', ['react']);
+
+import { checkFullLicense } from '../../../license/check_license';
+import { checkGetJobsPrivilege } from '../../../privilege/check_privilege';
+import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes';
+import { initPromise } from '../../../util/promise';
+
+import uiRoutes from 'ui/routes';
+
+const template = `
+
+
+
+
+`;
+
+uiRoutes
+ .when('/settings/calendars_list', {
+ template,
+ resolve: {
+ CheckLicense: checkFullLicense,
+ privileges: checkGetJobsPrivilege,
+ mlNodeCount: getMlNodeCount,
+ initPromise: initPromise(false)
+ }
+ });
+
+
+import { CalendarsList } from './calendars_list';
+
+module.directive('mlCalendarsList', function () {
+ return {
+ restrict: 'E',
+ replace: false,
+ scope: {},
+ link: function (scope, element) {
+ ReactDOM.render(
+ React.createElement(CalendarsList),
+ element[0]
+ );
+ }
+ };
+});
diff --git a/x-pack/plugins/ml/public/settings/calendars/list/index.js b/x-pack/plugins/ml/public/settings/calendars/list/index.js
new file mode 100644
index 0000000000000..fd75a9ceb9b49
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/list/index.js
@@ -0,0 +1,9 @@
+/*
+ * 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 './directive';
diff --git a/x-pack/plugins/ml/public/settings/calendars/list/table/__snapshots__/table.test.js.snap b/x-pack/plugins/ml/public/settings/calendars/list/table/__snapshots__/table.test.js.snap
new file mode 100644
index 0000000000000..4b3a3dab2f100
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/list/table/__snapshots__/table.test.js.snap
@@ -0,0 +1,110 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CalendarsListTable renders the table with all calendars 1`] = `
+
+
+ New
+ ,
+
+ Delete
+ ,
+ ],
+ }
+ }
+ selection={
+ Object {
+ "onSelectionChange": [Function],
+ }
+ }
+ sorting={
+ Object {
+ "sort": Object {
+ "direction": "asc",
+ "field": "calendar_id",
+ },
+ }
+ }
+ />
+
+`;
diff --git a/x-pack/plugins/ml/public/settings/calendars/list/table/index.js b/x-pack/plugins/ml/public/settings/calendars/list/table/index.js
new file mode 100644
index 0000000000000..c5c8489517452
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/list/table/index.js
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+
+export { CalendarsListTable } from './table';
diff --git a/x-pack/plugins/ml/public/settings/calendars/list/table/table.js b/x-pack/plugins/ml/public/settings/calendars/list/table/table.js
new file mode 100644
index 0000000000000..ea5b02b568ede
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/list/table/table.js
@@ -0,0 +1,132 @@
+/*
+ * 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,
+ EuiLink,
+ EuiInMemoryTable,
+} from '@elastic/eui';
+
+import chrome from 'ui/chrome';
+
+
+export function CalendarsListTable({
+ calendarsList,
+ onDeleteClick,
+ setSelectedCalendarList,
+ loading,
+ canCreateCalendar,
+ canDeleteCalendar,
+ mlNodesAvailable,
+ itemsSelected
+}) {
+
+ const sorting = {
+ sort: {
+ field: 'calendar_id',
+ direction: 'asc',
+ }
+ };
+
+ const pagination = {
+ initialPageSize: 20,
+ pageSizeOptions: [10, 20]
+ };
+
+ const columns = [
+ {
+ field: 'calendar_id',
+ name: 'ID',
+ sortable: true,
+ truncateText: true,
+ render: (id) => (
+
+ {id}
+
+ )
+ },
+ {
+ field: 'job_ids_string',
+ name: 'Jobs',
+ sortable: true,
+ truncateText: true,
+ },
+ {
+ field: 'events_length',
+ name: 'Events',
+ sortable: true
+ }
+ ];
+
+ const tableSelection = {
+ onSelectionChange: (selection) => setSelectedCalendarList(selection)
+ };
+
+ const search = {
+ toolsRight: [
+ (
+
+ New
+
+ ),
+ (
+
+ Delete
+
+ )
+ ],
+ box: {
+ incremental: true,
+ },
+ filters: []
+ };
+
+ return (
+
+
+
+ );
+}
+
+CalendarsListTable.propTypes = {
+ calendarsList: PropTypes.array.isRequired,
+ onDeleteClick: PropTypes.func.isRequired,
+ loading: PropTypes.bool.isRequired,
+ canCreateCalendar: PropTypes.bool.isRequired,
+ canDeleteCalendar: PropTypes.bool.isRequired,
+ mlNodesAvailable: PropTypes.bool.isRequired,
+ setSelectedCalendarList: PropTypes.func.isRequired,
+ itemsSelected: PropTypes.bool.isRequired,
+};
diff --git a/x-pack/plugins/ml/public/settings/calendars/list/table/table.test.js b/x-pack/plugins/ml/public/settings/calendars/list/table/table.test.js
new file mode 100644
index 0000000000000..0c0b9605a6e09
--- /dev/null
+++ b/x-pack/plugins/ml/public/settings/calendars/list/table/table.test.js
@@ -0,0 +1,94 @@
+/*
+ * 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, mount } from 'enzyme';
+import React from 'react';
+
+import { CalendarsListTable } from './table';
+
+
+jest.mock('ui/chrome', () => ({
+ getBasePath: jest.fn()
+}));
+
+const calendars = [
+ {
+ 'calendar_id': 'farequote-calendar',
+ 'job_ids': ['farequote'],
+ 'description': 'test ',
+ 'events': [] },
+ {
+ 'calendar_id': 'this-is-a-new-calendar',
+ 'job_ids': ['test'],
+ 'description': 'new calendar',
+ 'events': [] }];
+
+const props = {
+ calendarsList: calendars,
+ canCreateCalendar: true,
+ canDeleteCalendar: true,
+ itemsSelected: false,
+ loading: false,
+ mlNodesAvailable: true,
+ onDeleteClick: () => { },
+ setSelectedCalendarList: () => { }
+};
+
+describe('CalendarsListTable', () => {
+
+ test('renders the table with all calendars', () => {
+ const wrapper = shallow(
+
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('New button enabled if permission available', () => {
+ const wrapper = mount(
+
+ );
+
+ const buttons = wrapper.find('[data-testid="new_calendar_button"]');
+ const button = buttons.find('EuiButton');
+
+ expect(button.prop('isDisabled')).toEqual(false);
+ });
+
+ test('New button disabled if no permission available', () => {
+ const disableProps = {
+ ...props,
+ canCreateCalendar: false
+ };
+
+ const wrapper = mount(
+
+ );
+
+ const buttons = wrapper.find('[data-testid="new_calendar_button"]');
+ const button = buttons.find('EuiButton');
+
+ expect(button.prop('isDisabled')).toEqual(true);
+ });
+
+
+ test('New button disabled if no ML nodes available', () => {
+ const disableProps = {
+ ...props,
+ mlNodesAvailable: false
+ };
+
+ const wrapper = mount(
+
+ );
+
+ const buttons = wrapper.find('[data-testid="new_calendar_button"]');
+ const button = buttons.find('EuiButton');
+
+ expect(button.prop('isDisabled')).toEqual(true);
+ });
+
+});
diff --git a/x-pack/plugins/ml/public/settings/index.js b/x-pack/plugins/ml/public/settings/index.js
index 414587ae8d9ce..5faba118e050c 100644
--- a/x-pack/plugins/ml/public/settings/index.js
+++ b/x-pack/plugins/ml/public/settings/index.js
@@ -7,5 +7,5 @@
import './settings_controller';
-import './scheduled_events';
+import './calendars';
import './filter_lists';
diff --git a/x-pack/plugins/ml/public/settings/scheduled_events/calendars_list/__tests__/calendars_list_controller.js b/x-pack/plugins/ml/public/settings/scheduled_events/calendars_list/__tests__/calendars_list_controller.js
index 30121de63ac92..7e519be3ddaeb 100644
--- a/x-pack/plugins/ml/public/settings/scheduled_events/calendars_list/__tests__/calendars_list_controller.js
+++ b/x-pack/plugins/ml/public/settings/scheduled_events/calendars_list/__tests__/calendars_list_controller.js
@@ -9,7 +9,7 @@
import ngMock from 'ng_mock';
import expect from 'expect.js';
-describe('ML - Calendars List Controller', () => {
+xdescribe('ML - Calendars List Controller', () => {
beforeEach(() => {
ngMock.module('kibana');
});
diff --git a/x-pack/plugins/ml/public/settings/scheduled_events/components/import_events_modal/__tests__/import_events_modal_controller.js b/x-pack/plugins/ml/public/settings/scheduled_events/components/import_events_modal/__tests__/import_events_modal_controller.js
index 5db68ce991f42..7984eb93e0801 100644
--- a/x-pack/plugins/ml/public/settings/scheduled_events/components/import_events_modal/__tests__/import_events_modal_controller.js
+++ b/x-pack/plugins/ml/public/settings/scheduled_events/components/import_events_modal/__tests__/import_events_modal_controller.js
@@ -11,7 +11,7 @@ import expect from 'expect.js';
const mockModalInstance = { close: function () { }, dismiss: function () { } };
-describe('ML - Import Events Modal Controller', () => {
+xdescribe('ML - Import Events Modal Controller', () => {
beforeEach(() => {
ngMock.module('kibana');
});
diff --git a/x-pack/plugins/ml/public/settings/scheduled_events/components/new_event_modal/__tests__/new_event_modal_controller.js b/x-pack/plugins/ml/public/settings/scheduled_events/components/new_event_modal/__tests__/new_event_modal_controller.js
index 6c7b3cb85ba23..a6f29dc0939d8 100644
--- a/x-pack/plugins/ml/public/settings/scheduled_events/components/new_event_modal/__tests__/new_event_modal_controller.js
+++ b/x-pack/plugins/ml/public/settings/scheduled_events/components/new_event_modal/__tests__/new_event_modal_controller.js
@@ -11,7 +11,7 @@ import expect from 'expect.js';
const mockModalInstance = { close: function () { }, dismiss: function () { } };
-describe('ML - New Event Modal Controller', () => {
+xdescribe('ML - New Event Modal Controller', () => {
beforeEach(() => {
ngMock.module('kibana');
});
diff --git a/x-pack/plugins/ml/public/settings/scheduled_events/new_calendar/__tests__/create_calendar_controller.js b/x-pack/plugins/ml/public/settings/scheduled_events/new_calendar/__tests__/create_calendar_controller.js
index a8605d5f8d359..829354a2927e7 100644
--- a/x-pack/plugins/ml/public/settings/scheduled_events/new_calendar/__tests__/create_calendar_controller.js
+++ b/x-pack/plugins/ml/public/settings/scheduled_events/new_calendar/__tests__/create_calendar_controller.js
@@ -9,7 +9,7 @@
import ngMock from 'ng_mock';
import expect from 'expect.js';
-describe('ML - Create Calendar Controller', () => {
+xdescribe('ML - Create Calendar Controller', () => {
beforeEach(() => {
ngMock.module('kibana');
});